From 97b6835c92eb6a5574645fa151f8f687597cb0f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Tue, 22 Aug 2023 11:49:03 +0200 Subject: [PATCH 1/3] Define `BridgingDelegate` to enable component testing by mocking the component's delegate. --- Source/BridgeComponent.swift | 8 +-- Source/BridgeDelegate.swift | 29 +++++++++-- Strada.xcodeproj/project.pbxproj | 4 ++ Tests/BridgeComponentTests.swift | 77 ++++++++-------------------- Tests/Spies/BridgeComponentSpy.swift | 2 +- Tests/Spies/BridgeDelegateSpy.swift | 59 +++++++++++++++++++++ Tests/TestData.swift | 4 +- 7 files changed, 115 insertions(+), 68 deletions(-) create mode 100644 Tests/Spies/BridgeDelegateSpy.swift diff --git a/Source/BridgeComponent.swift b/Source/BridgeComponent.swift index c73fd3d..fe8ad0c 100644 --- a/Source/BridgeComponent.swift +++ b/Source/BridgeComponent.swift @@ -2,10 +2,10 @@ import Foundation protocol BridgingComponent: AnyObject { static var name: String { get } - var delegate: BridgeDelegate { get } + var delegate: BridgingDelegate { get } init(destination: BridgeDestination, - delegate: BridgeDelegate) + delegate: BridgingDelegate) func onReceive(message: Message) func onViewDidLoad() @@ -25,9 +25,9 @@ open class BridgeComponent: BridgingComponent { fatalError("BridgeComponent subclass must provide a unique 'name'") } - public unowned let delegate: BridgeDelegate + public unowned let delegate: BridgingDelegate - required public init(destination: BridgeDestination, delegate: BridgeDelegate) { + required public init(destination: BridgeDestination, delegate: BridgingDelegate) { self.delegate = delegate } diff --git a/Source/BridgeDelegate.swift b/Source/BridgeDelegate.swift index 60d5b6b..97c861e 100644 --- a/Source/BridgeDelegate.swift +++ b/Source/BridgeDelegate.swift @@ -3,7 +3,28 @@ import WebKit public protocol BridgeDestination: AnyObject {} -public final class BridgeDelegate { +public protocol BridgingDelegate: AnyObject { + var location: String { get } + var destination: BridgeDestination { get } + var webView: WKWebView? { get } + + func webViewDidBecomeActive(_ webView: WKWebView) + func webViewDidBecomeDeactivated() + func reply(with message: Message) -> Bool + + func onViewDidLoad() + func onViewWillAppear() + func onViewDidAppear() + func onViewWillDisappear() + func onViewDidDisappear() + + func component() -> C? + + func bridgeDidInitialize() + func bridgeDidReceiveMessage(_ message: Message) -> Bool +} + +public final class BridgeDelegate: BridgingDelegate { public let location: String public unowned let destination: BridgeDestination public var webView: WKWebView? { @@ -86,15 +107,15 @@ public final class BridgeDelegate { return activeComponents.compactMap { $0 as? C }.first } - // MARK: Internal + // MARK: Internal use - func bridgeDidInitialize() { + public func bridgeDidInitialize() { let componentNames = componentTypes.map { $0.name } bridge?.register(components: componentNames) } @discardableResult - func bridgeDidReceiveMessage(_ message: Message) -> Bool { + public func bridgeDidReceiveMessage(_ message: Message) -> Bool { guard destinationIsActive, location == message.metadata?.url else { logger.warning("bridgeDidIgnoreMessage: \(String(describing: message))") diff --git a/Strada.xcodeproj/project.pbxproj b/Strada.xcodeproj/project.pbxproj index 4fbda5d..64b2a5f 100644 --- a/Strada.xcodeproj/project.pbxproj +++ b/Strada.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ E20978492A71366B00CDEEE5 /* Data+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20978482A71366B00CDEEE5 /* Data+Utils.swift */; }; E209784B2A714D4E00CDEEE5 /* String+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = E209784A2A714D4E00CDEEE5 /* String+JSON.swift */; }; E209784D2A714F1900CDEEE5 /* Dictionary+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = E209784C2A714F1900CDEEE5 /* Dictionary+JSON.swift */; }; + E227FAEE2A94B35900A645E4 /* BridgeDelegateSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E227FAED2A94B35900A645E4 /* BridgeDelegateSpy.swift */; }; E22CBEFF2A84D7060024EFB8 /* StradaConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22CBEFE2A84D7060024EFB8 /* StradaConfig.swift */; }; E22CBF012A84DC380024EFB8 /* Strada.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22CBF002A84DC380024EFB8 /* Strada.swift */; }; E22CBF032A852A140024EFB8 /* UserAgentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22CBF022A852A140024EFB8 /* UserAgentTests.swift */; }; @@ -67,6 +68,7 @@ E20978482A71366B00CDEEE5 /* Data+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utils.swift"; sourceTree = ""; }; E209784A2A714D4E00CDEEE5 /* String+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+JSON.swift"; sourceTree = ""; }; E209784C2A714F1900CDEEE5 /* Dictionary+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+JSON.swift"; sourceTree = ""; }; + E227FAED2A94B35900A645E4 /* BridgeDelegateSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeDelegateSpy.swift; sourceTree = ""; }; E22CBEFE2A84D7060024EFB8 /* StradaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StradaConfig.swift; sourceTree = ""; }; E22CBF002A84DC380024EFB8 /* Strada.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strada.swift; sourceTree = ""; }; E22CBF022A852A140024EFB8 /* UserAgentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentTests.swift; sourceTree = ""; }; @@ -170,6 +172,7 @@ children = ( E2FDCF9A2A829AEE003D27AE /* BridgeSpy.swift */, E2FDCF9E2A829CA0003D27AE /* BridgeComponentSpy.swift */, + E227FAED2A94B35900A645E4 /* BridgeDelegateSpy.swift */, ); path = Spies; sourceTree = ""; @@ -309,6 +312,7 @@ buildActionMask = 2147483647; files = ( E2DB15952A72B0A8001EE08C /* BridgeDelegateTests.swift in Sources */, + E227FAEE2A94B35900A645E4 /* BridgeDelegateSpy.swift in Sources */, C11349C2258801F6000A6E56 /* JavaScriptTests.swift in Sources */, E22CBF032A852A140024EFB8 /* UserAgentTests.swift in Sources */, E2FDCF982A8297DA003D27AE /* BridgeComponentTests.swift in Sources */, diff --git a/Tests/BridgeComponentTests.swift b/Tests/BridgeComponentTests.swift index 5b4c428..91dd5d9 100644 --- a/Tests/BridgeComponentTests.swift +++ b/Tests/BridgeComponentTests.swift @@ -4,38 +4,24 @@ import WebKit @testable import Strada class BridgeComponentTest: XCTestCase { - private var delegate: BridgeDelegate! - private var bridge: BridgeSpy! + private var delegate: BridgeDelegateSpy! private var destination: AppBridgeDestination! - private var component: BridgeComponentSpy! + private var component: OneBridgeComponent! private let message = Message(id: "1", - component: BridgeComponentSpy.name, + component: OneBridgeComponent.name, event: "connect", metadata: .init(url: "https://37signals.com"), jsonData: "{\"title\":\"Page-title\",\"subtitle\":\"Page-subtitle\"}") override func setUp() async throws { destination = AppBridgeDestination() - delegate = BridgeDelegate(location: "https://37signals.com", - destination: destination, - componentTypes: [BridgeComponentSpy.self]) - - bridge = BridgeSpy() - bridge.delegate = delegate - delegate.bridge = bridge - delegate.onViewDidLoad() - - delegate.bridgeDidReceiveMessage(message) - component = delegate.component() + delegate = BridgeDelegateSpy() + component = OneBridgeComponent(destination: destination, delegate: delegate) + component.didReceive(message: message) } // MARK: didReceive(:) and caching - func test_didReceiveCallsOnReceive() { - XCTAssertTrue(component.onReceiveMessageWasCalled) - XCTAssertEqual(component.onReceiveMessageArg, message) - } - func test_didReceiveCachesTheMessage() { let cachedMessage = component.receivedMessage(for: "connect") XCTAssertEqual(cachedMessage, message) @@ -45,7 +31,7 @@ class BridgeComponentTest: XCTestCase { let newJsonData = "{\"title\":\"Page-title\"}" let newMessage = message.replacing(jsonData: newJsonData) - delegate.bridgeDidReceiveMessage(newMessage) + component.didReceive(message: newMessage) let cachedMessage = component.receivedMessage(for: "connect") XCTAssertEqual(cachedMessage, newMessage) @@ -62,8 +48,8 @@ class BridgeComponentTest: XCTestCase { let success = component.reply(to: "connect") XCTAssertTrue(success) - XCTAssertTrue(bridge.replyWithMessageWasCalled) - XCTAssertEqual(bridge.replyWithMessageArg, message) + XCTAssertTrue(delegate.replyWithMessageWasCalled) + XCTAssertEqual(delegate.replyWithMessageArg, message) } func test_replyToReceivedMessageWithACodableObjectSucceeds() { @@ -74,8 +60,8 @@ class BridgeComponentTest: XCTestCase { let success = component.reply(to: "connect", with: messageData) XCTAssertTrue(success) - XCTAssertTrue(bridge.replyWithMessageWasCalled) - XCTAssertEqual(bridge.replyWithMessageArg, newMessage) + XCTAssertTrue(delegate.replyWithMessageWasCalled) + XCTAssertEqual(delegate.replyWithMessageArg, newMessage) } func test_replyToMessageNotReceivedWithACodableObjectIgnoresTheReply() { @@ -84,36 +70,26 @@ class BridgeComponentTest: XCTestCase { let success = component.reply(to: "disconnect", with: messageData) XCTAssertFalse(success) - XCTAssertFalse(bridge.replyWithMessageWasCalled) - XCTAssertNil(bridge.replyWithMessageArg) + XCTAssertFalse(delegate.replyWithMessageWasCalled) + XCTAssertNil(delegate.replyWithMessageArg) } func test_replyToMessageNotReceivedIgnoresTheReply() { let success = component.reply(to: "disconnect") XCTAssertFalse(success) - XCTAssertFalse(bridge.replyWithMessageWasCalled) - XCTAssertNil(bridge.replyWithMessageArg) + XCTAssertFalse(delegate.replyWithMessageWasCalled) + XCTAssertNil(delegate.replyWithMessageArg) } func test_replyToMessageNotReceivedWithJsonDataIgnoresTheReply() { let success = component.reply(to: "disconnect", with: "{\"title\":\"Page-title\"}") XCTAssertFalse(success) - XCTAssertFalse(bridge.replyWithMessageWasCalled) - XCTAssertNil(bridge.replyWithMessageArg) + XCTAssertFalse(delegate.replyWithMessageWasCalled) + XCTAssertNil(delegate.replyWithMessageArg) } - - func test_replyToFailsWhenBridgeNotSet() { - delegate.bridge = nil - - let success = component.reply(to: "disconnect") - - XCTAssertFalse(success) - XCTAssertFalse(bridge.replyWithMessageWasCalled) - XCTAssertNil(bridge.replyWithMessageArg) - } - + // MARK: reply(with:) func test_replyWithSucceedsWhenBridgeIsSet() { @@ -123,20 +99,7 @@ class BridgeComponentTest: XCTestCase { let success = component.reply(with: newMessage) XCTAssertTrue(success) - XCTAssertTrue(bridge.replyWithMessageWasCalled) - XCTAssertEqual(bridge.replyWithMessageArg, newMessage) - } - - func test_replyWithFailsWhenBridgeNotSet() { - delegate.bridge = nil - - let newJsonData = "{\"title\":\"Page-title\"}" - let newMessage = message.replacing(jsonData: newJsonData) - - let success = component.reply(with: newMessage) - - XCTAssertFalse(success) - XCTAssertFalse(bridge.replyWithMessageWasCalled) - XCTAssertNil(bridge.replyWithMessageArg) + XCTAssertTrue(delegate.replyWithMessageWasCalled) + XCTAssertEqual(delegate.replyWithMessageArg, newMessage) } } diff --git a/Tests/Spies/BridgeComponentSpy.swift b/Tests/Spies/BridgeComponentSpy.swift index 444cf41..bf3b379 100644 --- a/Tests/Spies/BridgeComponentSpy.swift +++ b/Tests/Spies/BridgeComponentSpy.swift @@ -13,7 +13,7 @@ final class BridgeComponentSpy: BridgeComponent { var onViewWillDisappearWasCalled = false var onViewDidDisappearWasCalled = false - required init(destination: BridgeDestination, delegate: BridgeDelegate) { + required init(destination: BridgeDestination, delegate: BridgingDelegate) { super.init(destination: destination, delegate: delegate) } diff --git a/Tests/Spies/BridgeDelegateSpy.swift b/Tests/Spies/BridgeDelegateSpy.swift new file mode 100644 index 0000000..d3c0690 --- /dev/null +++ b/Tests/Spies/BridgeDelegateSpy.swift @@ -0,0 +1,59 @@ +import Foundation +import WebKit +@testable import Strada + +final class BridgeDelegateSpy: BridgingDelegate { + let location: String = "" + let destination: BridgeDestination = AppBridgeDestination() + var webView: WKWebView? = nil + + var replyWithMessageWasCalled = false + var replyWithMessageArg: Message? + + func webViewDidBecomeActive(_ webView: WKWebView) { + + } + + func webViewDidBecomeDeactivated() { + + } + + func reply(with message: Message) -> Bool { + replyWithMessageWasCalled = true + replyWithMessageArg = message + + return true + } + + func onViewDidLoad() { + + } + + func onViewWillAppear() { + + } + + func onViewDidAppear() { + + } + + func onViewWillDisappear() { + + } + + func onViewDidDisappear() { + + } + + func component() -> C? where C : BridgeComponent { + return nil + } + + func bridgeDidInitialize() { + + } + + func bridgeDidReceiveMessage(_ message: Message) -> Bool { + return false + } +} diff --git a/Tests/TestData.swift b/Tests/TestData.swift index 52100d5..3fb7893 100644 --- a/Tests/TestData.swift +++ b/Tests/TestData.swift @@ -6,7 +6,7 @@ final class AppBridgeDestination: BridgeDestination {} final class OneBridgeComponent: BridgeComponent { static override var name: String { "one" } - required init(destination: BridgeDestination, delegate: BridgeDelegate) { + required init(destination: BridgeDestination, delegate: BridgingDelegate) { super.init(destination: destination, delegate: delegate) } @@ -16,7 +16,7 @@ final class OneBridgeComponent: BridgeComponent { final class TwoBridgeComponent: BridgeComponent { static override var name: String { "two" } - required init(destination: BridgeDestination, delegate: BridgeDelegate) { + required init(destination: BridgeDestination, delegate: BridgingDelegate) { super.init(destination: destination, delegate: delegate) } From 861af0658cdc96f2d7c82640ce7b933b05c2c245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Tue, 22 Aug 2023 15:06:15 +0200 Subject: [PATCH 2/3] Add example component tests. --- Strada.xcodeproj/project.pbxproj | 16 ++++ .../ComposerComponent.swift | 77 +++++++++++++++++++ .../ComposerComponentTests.swift | 76 ++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 Tests/ComponentTestExample/ComposerComponent.swift create mode 100644 Tests/ComponentTestExample/ComposerComponentTests.swift diff --git a/Strada.xcodeproj/project.pbxproj b/Strada.xcodeproj/project.pbxproj index 64b2a5f..12ca970 100644 --- a/Strada.xcodeproj/project.pbxproj +++ b/Strada.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ E209784B2A714D4E00CDEEE5 /* String+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = E209784A2A714D4E00CDEEE5 /* String+JSON.swift */; }; E209784D2A714F1900CDEEE5 /* Dictionary+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = E209784C2A714F1900CDEEE5 /* Dictionary+JSON.swift */; }; E227FAEE2A94B35900A645E4 /* BridgeDelegateSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E227FAED2A94B35900A645E4 /* BridgeDelegateSpy.swift */; }; + E227FAF02A94D34E00A645E4 /* ComposerComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E227FAEF2A94D34E00A645E4 /* ComposerComponent.swift */; }; + E227FAF32A94D57300A645E4 /* ComposerComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E227FAF22A94D57300A645E4 /* ComposerComponentTests.swift */; }; E22CBEFF2A84D7060024EFB8 /* StradaConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22CBEFE2A84D7060024EFB8 /* StradaConfig.swift */; }; E22CBF012A84DC380024EFB8 /* Strada.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22CBF002A84DC380024EFB8 /* Strada.swift */; }; E22CBF032A852A140024EFB8 /* UserAgentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22CBF022A852A140024EFB8 /* UserAgentTests.swift */; }; @@ -69,6 +71,8 @@ E209784A2A714D4E00CDEEE5 /* String+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+JSON.swift"; sourceTree = ""; }; E209784C2A714F1900CDEEE5 /* Dictionary+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+JSON.swift"; sourceTree = ""; }; E227FAED2A94B35900A645E4 /* BridgeDelegateSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeDelegateSpy.swift; sourceTree = ""; }; + E227FAEF2A94D34E00A645E4 /* ComposerComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerComponent.swift; sourceTree = ""; }; + E227FAF22A94D57300A645E4 /* ComposerComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerComponentTests.swift; sourceTree = ""; }; E22CBEFE2A84D7060024EFB8 /* StradaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StradaConfig.swift; sourceTree = ""; }; E22CBF002A84DC380024EFB8 /* Strada.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strada.swift; sourceTree = ""; }; E22CBF022A852A140024EFB8 /* UserAgentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentTests.swift; sourceTree = ""; }; @@ -142,6 +146,7 @@ 9274F1F22229963B003E85F4 /* Tests */ = { isa = PBXGroup; children = ( + E227FAF12A94D48C00A645E4 /* ComponentTestExample */, E2FDCF9C2A829C6F003D27AE /* TestData.swift */, E2FDCF992A829AD5003D27AE /* Spies */, C1EB052D2588201600933244 /* BridgeTests.swift */, @@ -167,6 +172,15 @@ path = Extensions; sourceTree = ""; }; + E227FAF12A94D48C00A645E4 /* ComponentTestExample */ = { + isa = PBXGroup; + children = ( + E227FAEF2A94D34E00A645E4 /* ComposerComponent.swift */, + E227FAF22A94D57300A645E4 /* ComposerComponentTests.swift */, + ); + path = ComponentTestExample; + sourceTree = ""; + }; E2FDCF992A829AD5003D27AE /* Spies */ = { isa = PBXGroup; children = ( @@ -312,8 +326,10 @@ buildActionMask = 2147483647; files = ( E2DB15952A72B0A8001EE08C /* BridgeDelegateTests.swift in Sources */, + E227FAF02A94D34E00A645E4 /* ComposerComponent.swift in Sources */, E227FAEE2A94B35900A645E4 /* BridgeDelegateSpy.swift in Sources */, C11349C2258801F6000A6E56 /* JavaScriptTests.swift in Sources */, + E227FAF32A94D57300A645E4 /* ComposerComponentTests.swift in Sources */, E22CBF032A852A140024EFB8 /* UserAgentTests.swift in Sources */, E2FDCF982A8297DA003D27AE /* BridgeComponentTests.swift in Sources */, E2FDCF9D2A829C6F003D27AE /* TestData.swift in Sources */, diff --git a/Tests/ComponentTestExample/ComposerComponent.swift b/Tests/ComponentTestExample/ComposerComponent.swift new file mode 100644 index 0000000..bd16dc7 --- /dev/null +++ b/Tests/ComponentTestExample/ComposerComponent.swift @@ -0,0 +1,77 @@ +import Foundation +import XCTest +@testable import Strada + +final class ComposerComponent: BridgeComponent { + static override var name: String { "composer" } + + override func onReceive(message: Message) { + guard let event = InboundEvent(rawValue: message.event) else { + return + } + + switch event { + case .connect: + break + } + } + + func selectSender(emailAddress: String) { + guard let message = receivedMessage(for: InboundEvent.connect.rawValue), + let data: MessageData = message.data() else { + return + } + + guard let sender = data.senders.first(where: { $0.email == emailAddress }) else { + return + } + + let newMessage = message.replacing(event: OutboundEvent.selectSender.rawValue, + data: SelectSenderMessageData(selectedIndex: sender.index)) + reply(with: newMessage) + } + + func selectedSender() -> String? { + guard let message = receivedMessage(for: InboundEvent.connect.rawValue), + let data: MessageData = message.data() else { + return nil + } + + guard let selected = data.senders.first(where: { $0.selected }) else { + return nil + } + + return selected.email + } +} + +// MARK: Events + +extension ComposerComponent { + private enum InboundEvent: String { + case connect + } + + private enum OutboundEvent: String { + case selectSender = "select-sender" + } +} + +// MARK: Message data + +extension ComposerComponent { + private struct MessageData: Decodable { + let senders: [Sender] + } + + private struct Sender: Decodable { + let email: String + let index: Int + let selected: Bool + } + + private struct SelectSenderMessageData: Encodable { + let selectedIndex: Int + } +} + diff --git a/Tests/ComponentTestExample/ComposerComponentTests.swift b/Tests/ComponentTestExample/ComposerComponentTests.swift new file mode 100644 index 0000000..b916b8c --- /dev/null +++ b/Tests/ComponentTestExample/ComposerComponentTests.swift @@ -0,0 +1,76 @@ +import XCTest +import WebKit +@testable import Strada + +final class ComposerComponentTests: XCTestCase { + private var delegate: BridgeDelegateSpy! + private var destination: AppBridgeDestination! + private var component: ComposerComponent! + private lazy var connectMessage = Message(id: "1", + component: ComposerComponent.name, + event: "connect", + metadata: .init(url: "https://37signals.com"), + jsonData: connectMessageJsonData) + private let connectMessageJsonData = """ + [ + { + "email":"user@37signals.com", + "index":0, + "selected":true + }, + { + "email":"user1@37signals.com", + "index":1, + "selected":false + }, + { + "email":"user2@37signals.com", + "index":2, + "selected":false + } + ] + """ + + override func setUp() async throws { + delegate = BridgeDelegateSpy() + destination = AppBridgeDestination() + component = ComposerComponent(destination: destination, delegate: delegate) + } + + // MARK: Retreive sender tests + + func test_connectMessageContainsSelectedSender() { + component.onReceive(message: connectMessage) + + XCTAssertEqual(component.selectedSender(), "user@37signals.com") + } + + // MARK: Select sender tests + + func test_selectSender_emailFound_sendsTheCorrectMessageReply() { + component.onReceive(message: connectMessage) + + component.selectSender(emailAddress: "user1@37signals.com") + + XCTAssertTrue(delegate.replyWithMessageWasCalled) + let expectedMessage = connectMessage.replacing(event: "select-sender", + jsonData: "{\"selectSender\":1}") + XCTAssertEqual(delegate.replyWithMessageArg, expectedMessage) + } + + func test_selectSender_emailNotFound_doesNotSendAnyMessage() { + component.onReceive(message: connectMessage) + + component.selectSender(emailAddress: "test@37signals.com") + + XCTAssertFalse(delegate.replyWithMessageWasCalled) + XCTAssertNil(delegate.replyWithMessageArg) + } + + func test_selectSender_beforeConnectMessage_doesNotSendAnyMessage() { + component.selectSender(emailAddress: "user1@37signals.com") + + XCTAssertFalse(delegate.replyWithMessageWasCalled) + XCTAssertNil(delegate.replyWithMessageArg) + } +} From 22ac2df140b4832c7a0dc2a15a416f65277f7e21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Tue, 22 Aug 2023 16:43:28 +0200 Subject: [PATCH 3/3] Expose, by making `didReceive(message:)` and all of component's view lifecycle functions public. This allows to use them for testing. --- Source/BridgeComponent.swift | 62 ++++++++++++++----- .../ComposerComponent.swift | 13 ++-- .../ComposerComponentTests.swift | 10 +-- 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/Source/BridgeComponent.swift b/Source/BridgeComponent.swift index fe8ad0c..de9343e 100644 --- a/Source/BridgeComponent.swift +++ b/Source/BridgeComponent.swift @@ -13,6 +13,13 @@ protocol BridgingComponent: AnyObject { func onViewDidAppear() func onViewWillDisappear() func onViewDidDisappear() + + func didReceive(message: Message) + func viewDidLoad() + func viewWillAppear() + func viewDidAppear() + func viewWillDisappear() + func viewDidDisappear() } open class BridgeComponent: BridgingComponent { @@ -31,6 +38,13 @@ open class BridgeComponent: BridgingComponent { self.delegate = delegate } + /// Called when a message is received from the web bridge. + /// Handle the message for its `event` type for the custom component's behavior. + /// - Parameter message: The `message` received from the web bridge. + open func onReceive(message: Message) { + fatalError("BridgeComponent subclass must handle incoming messages") + } + @discardableResult /// Replies to the web with a received message, optionally replacing its `event` or `jsonData`. /// @@ -56,7 +70,6 @@ open class BridgeComponent: BridgingComponent { return reply(with: message) } - @discardableResult /// Replies to the web with the last received message for a given `event`, replacing its `jsonData`. /// @@ -105,13 +118,6 @@ open class BridgeComponent: BridgingComponent { return receivedMessages[event] } - /// Called when a message is received from the web bridge. - /// Handle the message for its `event` type for the custom component's behavior. - /// - Parameter message: The `message` received from the web bridge. - open func onReceive(message: Message) { - fatalError("BridgeComponent subclass must handle incoming messages") - } - /// Called when the component's destination view is loaded into memory /// (and is active) based on its lifecycle events. /// You can use this as an opportunity to update the component's state/view. @@ -137,30 +143,54 @@ open class BridgeComponent: BridgingComponent { /// You can use this as an opportunity to update the component's state/view. open func onViewDidDisappear() {} - // MARK: Internal - - func didReceive(message: Message) { + /// This passes a received message to `onReceive(message:)`, caching it + /// for use with `reply(to: with:)` and `receivedMessage(for:)`. + /// + /// NOTE: This should not be called directly from within a component, + /// but is available to use for testing. + /// - Parameter message: The `message` received from the web bridge. + public func didReceive(message: Message) { receivedMessages[message.event] = message onReceive(message: message) } - func viewDidLoad() { + /// This passes the `viewDidLoad` lifecycle event to `onViewDidLoad()`. + /// + /// NOTE: This should not be called directly from within a component, + /// but is available to use for testing. + public func viewDidLoad() { onViewDidLoad() } - func viewWillAppear() { + /// This passes the `viewWillAppear` lifecycle event to `onViewWillAppear()`. + /// + /// NOTE: This should not be called directly from within a component, + /// but is available to use for testing. + public func viewWillAppear() { onViewWillAppear() } - func viewDidAppear() { + /// This passes the `viewDidAppear` lifecycle event to `onViewDidAppear()`. + /// + /// NOTE: This should not be called directly from within a component, + /// but is available to use for testing. + public func viewDidAppear() { onViewDidAppear() } - func viewWillDisappear() { + /// This passes the `viewWillDisappear` lifecycle event to `onViewWillDisappear()`. + /// + /// NOTE: This should not be called directly from within a component, + /// but is available to use for testing. + public func viewWillDisappear() { onViewWillDisappear() } - func viewDidDisappear() { + /// This passes the `viewDidDisappear` lifecycle event to `onViewDidDisappear()`. + /// + /// NOTE: This should not be called directly from within a component, + /// but is available to use for testing. + public func viewDidDisappear() { onViewDidDisappear() } diff --git a/Tests/ComponentTestExample/ComposerComponent.swift b/Tests/ComponentTestExample/ComposerComponent.swift index bd16dc7..a58901f 100644 --- a/Tests/ComponentTestExample/ComposerComponent.swift +++ b/Tests/ComponentTestExample/ComposerComponent.swift @@ -12,17 +12,18 @@ final class ComposerComponent: BridgeComponent { switch event { case .connect: + // Handle connect event if needed. break } } func selectSender(emailAddress: String) { guard let message = receivedMessage(for: InboundEvent.connect.rawValue), - let data: MessageData = message.data() else { + let senders: [Sender] = message.data() else { return } - guard let sender = data.senders.first(where: { $0.email == emailAddress }) else { + guard let sender = senders.first(where: { $0.email == emailAddress }) else { return } @@ -33,11 +34,11 @@ final class ComposerComponent: BridgeComponent { func selectedSender() -> String? { guard let message = receivedMessage(for: InboundEvent.connect.rawValue), - let data: MessageData = message.data() else { + let senders: [Sender] = message.data() else { return nil } - guard let selected = data.senders.first(where: { $0.selected }) else { + guard let selected = senders.first(where: { $0.selected }) else { return nil } @@ -60,10 +61,6 @@ extension ComposerComponent { // MARK: Message data extension ComposerComponent { - private struct MessageData: Decodable { - let senders: [Sender] - } - private struct Sender: Decodable { let email: String let index: Int diff --git a/Tests/ComponentTestExample/ComposerComponentTests.swift b/Tests/ComponentTestExample/ComposerComponentTests.swift index b916b8c..4a81d09 100644 --- a/Tests/ComponentTestExample/ComposerComponentTests.swift +++ b/Tests/ComponentTestExample/ComposerComponentTests.swift @@ -40,7 +40,7 @@ final class ComposerComponentTests: XCTestCase { // MARK: Retreive sender tests func test_connectMessageContainsSelectedSender() { - component.onReceive(message: connectMessage) + component.didReceive(message: connectMessage) XCTAssertEqual(component.selectedSender(), "user@37signals.com") } @@ -48,18 +48,18 @@ final class ComposerComponentTests: XCTestCase { // MARK: Select sender tests func test_selectSender_emailFound_sendsTheCorrectMessageReply() { - component.onReceive(message: connectMessage) + component.didReceive(message: connectMessage) component.selectSender(emailAddress: "user1@37signals.com") - XCTAssertTrue(delegate.replyWithMessageWasCalled) let expectedMessage = connectMessage.replacing(event: "select-sender", - jsonData: "{\"selectSender\":1}") + jsonData: "{\"selectedIndex\":1}") + XCTAssertTrue(delegate.replyWithMessageWasCalled) XCTAssertEqual(delegate.replyWithMessageArg, expectedMessage) } func test_selectSender_emailNotFound_doesNotSendAnyMessage() { - component.onReceive(message: connectMessage) + component.didReceive(message: connectMessage) component.selectSender(emailAddress: "test@37signals.com")