diff --git a/.gitignore b/.gitignore index 3b8d4a9..419f575 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ node_modules *.log *.xcuserstate + +*.xcbkptlist diff --git a/Source/Bridge.swift b/Source/Bridge.swift index f1eca46..ad1be95 100644 --- a/Source/Bridge.swift +++ b/Source/Bridge.swift @@ -1,70 +1,66 @@ import Foundation import WebKit -public protocol BridgeDelegate: AnyObject { - func bridgeDidInitialize() - func bridgeDidReceiveMessage(_ message: Message) -} - public enum BridgeError: Error { case missingWebView } +protocol Bridgable: AnyObject { + var delegate: BridgeDelegate? { get set } + var webView: WKWebView? { get } + + func register(component: String) + func register(components: [String]) + func unregister(component: String) + func send(_ message: Message) +} + /// `Bridge` is the object for configuring a web view and /// the channel for sending/receiving messages -public final class Bridge { - public typealias CompletionHandler = (_ result: Any?, _ error: Error?) -> Void +public final class Bridge: Bridgable { + typealias CompletionHandler = (_ result: Any?, _ error: Error?) -> Void - public var webView: WKWebView? { - didSet { - guard webView != oldValue else { return } - loadIntoWebView() + weak var delegate: BridgeDelegate? + weak var webView: WKWebView? + + public static func initialize(_ webView: WKWebView) { + if getBridgeFor(webView) == nil { + initialize(Bridge(webView: webView)) } } - public weak var delegate: BridgeDelegate? - - /// This needs to match whatever the JavaScript file uses - private let bridgeGlobal = "window.nativeBridge" - - /// The webkit.messageHandlers name - private let scriptHandlerName = "strada" + init(webView: WKWebView) { + self.webView = webView + loadIntoWebView() + } deinit { webView?.configuration.userContentController.removeScriptMessageHandler(forName: scriptHandlerName) } - - /// Create a new Bridge object for calling methods on this web view with a delegate - /// for receiving messages - public init(webView: WKWebView? = nil, delegate: BridgeDelegate? = nil) { - self.webView = webView - self.delegate = delegate - loadIntoWebView() - } - // MARK: - API + // MARK: - Internal API /// Register a single component /// - Parameter component: Name of a component to register support for - public func register(component: String) { + func register(component: String) { callBridgeFunction("register", arguments: [component]) } /// Register multiple components /// - Parameter components: Array of component names to register - public func register(components: [String]) { + func register(components: [String]) { callBridgeFunction("register", arguments: [components]) } /// Unregister support for a single component /// - Parameter component: Component name - public func unregister(component: String) { + func unregister(component: String) { callBridgeFunction("unregister", arguments: [component]) } /// Send a message through the bridge to the web application /// - Parameter message: Message to send - public func send(_ message: Message) { + func send(_ message: Message) { let internalMessage = InternalMessage(from: message) callBridgeFunction("send", arguments: [internalMessage.toJSON()]) } @@ -77,6 +73,47 @@ public final class Bridge { // let replyMessage = message.replacing(data: data) // callBridgeFunction("send", arguments: [replyMessage.toJSON()]) // } + + /// Evaluates javaScript string directly as passed in sending through the web view + func evaluate(javaScript: String, completion: CompletionHandler? = nil) { + guard let webView = webView else { + completion?(nil, BridgeError.missingWebView) + return + } + + webView.evaluateJavaScript(javaScript) { result, error in + if let error = error { + debugLog("Error evaluating JavaScript: \(error)") + } + + completion?(result, error) + } + } + + /// Evaluates a JavaScript function with optional arguments by encoding the arguments + /// Function should not include the parens + /// Usage: evaluate(function: "console.log", arguments: ["test"]) + func evaluate(function: String, arguments: [Any] = [], completion: CompletionHandler? = nil) { + evaluate(javaScript: JavaScript(functionName: function, arguments: arguments), completion: completion) + } + + static func initialize(_ bridge: Bridge) { + instances.append(bridge) + instances.removeAll { $0.webView == nil } + } + + static func getBridgeFor(_ webView: WKWebView) -> Bridge? { + return instances.first { $0.webView == webView } + } + + // MARK: Private + + private static var instances: [Bridge] = [] + /// This needs to match whatever the JavaScript file uses + private let bridgeGlobal = "window.nativeBridge" + + /// The webkit.messageHandlers name + private let scriptHandlerName = "strada" private func callBridgeFunction(_ function: String, arguments: [Any]) { let js = JavaScript(object: bridgeGlobal, functionName: function, arguments: arguments) @@ -94,7 +131,8 @@ public final class Bridge { configuration.userContentController.addUserScript(userScript) } - configuration.userContentController.add(ScriptMessageHandler(delegate: self), name: scriptHandlerName) + let scriptMessageHandler = ScriptMessageHandler(delegate: self) + configuration.userContentController.add(scriptMessageHandler, name: scriptHandlerName) } private func makeUserScript() -> WKUserScript? { @@ -111,31 +149,8 @@ public final class Bridge { return nil } } - - // MARK: - JavaScript Evaluation - - /// Evaluates javaScript string directly as passed in sending through the web view - public func evaluate(javaScript: String, completion: CompletionHandler? = nil) { - guard let webView = webView else { - completion?(nil, BridgeError.missingWebView) - return - } - - webView.evaluateJavaScript(javaScript) { result, error in - if let error = error { - debugLog("Error evaluating JavaScript: \(error)") - } - - completion?(result, error) - } - } - /// Evaluates a JavaScript function with optional arguments by encoding the arguments - /// Function should not include the parens - /// Usage: evaluate(function: "console.log", arguments: ["test"]) - public func evaluate(function: String, arguments: [Any] = [], completion: CompletionHandler? = nil) { - evaluate(javaScript: JavaScript(functionName: function, arguments: arguments), completion: completion) - } + // MARK: - JavaScript Evaluation private func evaluate(javaScript: JavaScript, completion: CompletionHandler? = nil) { do { @@ -149,12 +164,17 @@ public final class Bridge { extension Bridge: ScriptMessageHandlerDelegate { func scriptMessageHandlerDidReceiveMessage(_ scriptMessage: WKScriptMessage) { - if let event = scriptMessage.body as? String, event == "ready" { + if let event = scriptMessage.body as? String, + event == "ready" { delegate?.bridgeDidInitialize() - } else if let message = InternalMessage(scriptMessage: scriptMessage) { + return + } + + if let message = InternalMessage(scriptMessage: scriptMessage) { delegate?.bridgeDidReceiveMessage(message.toMessage()) - } else { - debugLog("Unhandled message received: \(scriptMessage.body)") + return } + + debugLog("Unhandled message received: \(scriptMessage.body)") } } diff --git a/Source/BridgeComponent.swift b/Source/BridgeComponent.swift new file mode 100644 index 0000000..302ea47 --- /dev/null +++ b/Source/BridgeComponent.swift @@ -0,0 +1,47 @@ +import Foundation + +protocol BridgingComponent: AnyObject { + static var name: String { get } + var delegate: BridgeDelegate { get } + + init(destination: BridgeDestination, + delegate: BridgeDelegate) + + func handle(message: Message) + func onViewDidLoad() + func onViewWillAppear() + func onViewDidAppear() + func onViewWillDisappear() + func onViewDidDisappear() +} + +open class BridgeComponent: BridgingComponent { + open class var name: String { + fatalError("BridgeComponent subclass must provide a unique 'name'") + } + + public unowned let delegate: BridgeDelegate + + required public init(destination: BridgeDestination, delegate: BridgeDelegate) { + self.delegate = delegate + } + + open func handle(message: Message) { + fatalError("BridgeComponent subclass must handle incoming messages") + } + + public func send(message: Message) { + guard let bridge = delegate.bridge else { + debugLog("bridgeMessageFailedToSend: bridge is not available") + return + } + + bridge.send(message) + } + + open func onViewDidLoad() {} + open func onViewWillAppear() {} + open func onViewDidAppear() {} + open func onViewWillDisappear() {} + open func onViewDidDisappear() {} +} diff --git a/Source/BridgeDelegate.swift b/Source/BridgeDelegate.swift new file mode 100644 index 0000000..00ca33a --- /dev/null +++ b/Source/BridgeDelegate.swift @@ -0,0 +1,120 @@ +import Foundation +import WebKit + +public protocol BridgeDestination: AnyObject {} + +public final class BridgeDelegate { + public let location: String + public unowned let destination: BridgeDestination + public var webView: WKWebView? { + bridge?.webView + } + + weak var bridge: Bridgable? + + public init(location: String, + destination: BridgeDestination, + componentTypes: [BridgeComponent.Type]) { + self.location = location + self.destination = destination + self.componentTypes = componentTypes + } + + public func webViewDidBecomeActive(_ webView: WKWebView) { + bridge = Bridge.getBridgeFor(webView) + bridge?.delegate = self + + if bridge == nil { + debugLog("bridgeNotInitializedForWebView") + } + } + + public func webViewDidBecomeDeactivated() { + bridge?.delegate = nil + bridge = nil + } + + // MARK: - Destination lifecycle + + public func onViewDidLoad() { + debugLog("bridgeDestinationViewDidLoad: \(location)") + destinationIsActive = true + activeComponents.forEach { $0.onViewDidLoad() } + } + + public func onViewWillAppear() { + debugLog("bridgeDestinationViewWillAppear: \(location)") + destinationIsActive = true + activeComponents.forEach { $0.onViewWillAppear() } + } + + public func onViewDidAppear() { + debugLog("bridgeDestinationViewDidAppear: \(location)") + destinationIsActive = true + activeComponents.forEach { $0.onViewDidAppear() } + } + + public func onViewWillDisappear() { + activeComponents.forEach { $0.onViewWillDisappear() } + debugLog("bridgeDestinationViewWillDisappear: \(location)") + } + + public func onViewDidDisappear() { + activeComponents.forEach { $0.onViewDidDisappear() } + destinationIsActive = false + debugLog("bridgeDestinationViewDidDisappear: \(location)") + } + + // MARK: Retrieve component by type + + public func component() -> C? { + return activeComponents.compactMap { $0 as? C }.first + } + + // MARK: Internal + + func bridgeDidInitialize() { + let componentNames = componentTypes.map { $0.name } + bridge?.register(components: componentNames) + } + + @discardableResult + func bridgeDidReceiveMessage(_ message: Message) -> Bool { + guard destinationIsActive, + location == message.metadata?.url else { + debugLog("bridgeDidIgnoreMessage: \(message)") + return false + } + + debugLog("bridgeDidReceiveMessage: \(message)") + getOrCreateComponent(name: message.component)?.handle(message: message) + + return true + } + + // MARK: Private + + private var initializedComponents: [String: BridgeComponent] = [:] + private var destinationIsActive = false + private let componentTypes: [BridgeComponent.Type] + + private var activeComponents: [BridgeComponent] { + return initializedComponents.values.filter { _ in destinationIsActive } + } + + private func getOrCreateComponent(name: String) -> BridgeComponent? { + if let component = initializedComponents[name] { + return component + } + + guard let componentType = componentTypes.first(where: { $0.name == name }) else { + return nil + } + + let component = componentType.init(destination: destination, delegate: self) + initializedComponents[name] = component + + return component + } +} + diff --git a/Strada.xcodeproj/project.pbxproj b/Strada.xcodeproj/project.pbxproj index ffa585b..1788947 100644 --- a/Strada.xcodeproj/project.pbxproj +++ b/Strada.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 53; objects = { /* Begin PBXBuildFile section */ @@ -24,6 +24,9 @@ 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 */; }; + E2DB15912A7163B0001EE08C /* BridgeDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DB15902A7163B0001EE08C /* BridgeDelegate.swift */; }; + E2DB15932A7282CF001EE08C /* BridgeComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DB15922A7282CF001EE08C /* BridgeComponent.swift */; }; + E2DB15952A72B0A8001EE08C /* BridgeDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DB15942A72B0A8001EE08C /* BridgeDelegateTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -57,6 +60,9 @@ 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 = ""; }; + E2DB15902A7163B0001EE08C /* BridgeDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeDelegate.swift; sourceTree = ""; }; + E2DB15922A7282CF001EE08C /* BridgeComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeComponent.swift; sourceTree = ""; }; + E2DB15942A72B0A8001EE08C /* BridgeDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeDelegateTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -109,6 +115,8 @@ 9274F1FF2229970D003E85F4 /* strada.js */, 9274F1E92229963B003E85F4 /* Info.plist */, E20978412A6E9E6B00CDEEE5 /* InternalMessage.swift */, + E2DB15902A7163B0001EE08C /* BridgeDelegate.swift */, + E2DB15922A7282CF001EE08C /* BridgeComponent.swift */, ); path = Source; sourceTree = ""; @@ -121,6 +129,7 @@ C1EB05252588133D00933244 /* MessageTests.swift */, 9274F1F52229963B003E85F4 /* Info.plist */, E20978432A6EAF3600CDEEE5 /* InternalMessageTests.swift */, + E2DB15942A72B0A8001EE08C /* BridgeDelegateTests.swift */, ); path = Tests; sourceTree = ""; @@ -191,8 +200,9 @@ 9274F1DC2229963B003E85F4 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1010; - LastUpgradeCheck = 1210; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = Basecamp; TargetAttributes = { 9274F1E42229963B003E85F4 = { @@ -250,8 +260,10 @@ E209784B2A714D4E00CDEEE5 /* String+JSON.swift in Sources */, C11349A62587EFFB000A6E56 /* ScriptMessageHandler.swift in Sources */, E20978472A7135E700CDEEE5 /* Encodable+Utils.swift in Sources */, + E2DB15912A7163B0001EE08C /* BridgeDelegate.swift in Sources */, C11349B22587F31E000A6E56 /* JavaScript.swift in Sources */, 9274F20222299715003E85F4 /* Bridge.swift in Sources */, + E2DB15932A7282CF001EE08C /* BridgeComponent.swift in Sources */, E20978492A71366B00CDEEE5 /* Data+Utils.swift in Sources */, 9274F20422299738003E85F4 /* Message.swift in Sources */, E20978422A6E9E6B00CDEEE5 /* InternalMessage.swift in Sources */, @@ -265,6 +277,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E2DB15952A72B0A8001EE08C /* BridgeDelegateTests.swift in Sources */, C11349C2258801F6000A6E56 /* JavaScriptTests.swift in Sources */, C1EB052E2588201600933244 /* BridgeTests.swift in Sources */, E20978442A6EAF3600CDEEE5 /* InternalMessageTests.swift in Sources */, @@ -336,7 +349,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -395,7 +408,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -418,6 +431,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -425,6 +439,8 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; PRODUCT_BUNDLE_IDENTIFIER = dev.hotwired.strada; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -445,6 +461,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -452,6 +469,8 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; PRODUCT_BUNDLE_IDENTIFIER = dev.hotwired.strada; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; diff --git a/Strada.xcodeproj/xcshareddata/xcschemes/Strada.xcscheme b/Strada.xcodeproj/xcshareddata/xcschemes/Strada.xcscheme index 28e3342..a43b636 100644 --- a/Strada.xcodeproj/xcshareddata/xcschemes/Strada.xcscheme +++ b/Strada.xcodeproj/xcshareddata/xcschemes/Strada.xcscheme @@ -1,6 +1,6 @@ - - diff --git a/Tests/BridgeDelegateTests.swift b/Tests/BridgeDelegateTests.swift new file mode 100644 index 0000000..d1ad6d7 --- /dev/null +++ b/Tests/BridgeDelegateTests.swift @@ -0,0 +1,242 @@ +import Foundation +import XCTest +import WebKit +@testable import Strada + +class BridgeDelegateTests: XCTestCase { + private var delegate: BridgeDelegate! + private var destination: BridgeDestinationSpy! + private var bridge: BridgeSpy! + private let json = """ + {"title":"Page-title","subtitle":"Page-subtitle"} + """ + + override func setUp() async throws { + destination = BridgeDestinationSpy() + delegate = BridgeDelegate(location: "https://37signals.com", + destination: destination, + componentTypes: [OneBridgeComponent.self, BridgeComponentSpy.self]) + + bridge = BridgeSpy() + delegate.bridge = bridge + delegate.onViewDidLoad() + } + + func testBridgeDidInitialize() { + delegate.bridgeDidInitialize() + + XCTAssertTrue(bridge.registerComponentsWasCalled) + XCTAssertEqual(bridge.registerComponentsArg, ["one", "two"]) + + // Registered components are lazy initialized. + let componentOne: BridgeComponentSpy? = delegate.component() + let componentTwo: BridgeComponentSpy? = delegate.component() + XCTAssertNil(componentOne) + XCTAssertNil(componentTwo) + } + + func testBridgeDidReceiveMessage() { + let json = """ + {"title":"Page-title","subtitle":"Page-subtitle"} + """ + let message = Message(id: "1", + component: "two", + event: "connect", + metadata: .init(url: "https://37signals.com"), + jsonData: json) + + var component: BridgeComponentSpy? = delegate.component() + + XCTAssertNil(component) + XCTAssertTrue(delegate.bridgeDidReceiveMessage(message)) + + component = delegate.component() + + XCTAssertNotNil(component) + // Make sure the component has delegate set, and did receive the message. + XCTAssertTrue(component!.handleMessageWasCalled) + XCTAssertEqual(component?.handleMessageArg, message) + XCTAssertNotNil(component?.delegate) + } + + func testBridgeIgnoresMessageForUnknownComponent() { + let json = """ + {"title":"Page-title","subtitle":"Page-subtitle"} + """ + let message = Message(id: "1", + component: "page", + event: "connect", + metadata: .init(url: "https://37signals.com/another_url"), + jsonData: json) + + XCTAssertFalse(delegate.bridgeDidReceiveMessage(message)) + } + + func testBridgeIgnoresMessageForInactiveDestination() { + let message = Message(id: "1", + component: "one", + event: "connect", + metadata: .init(url: "https://37signals.com"), + jsonData: json) + + XCTAssertTrue(delegate.bridgeDidReceiveMessage(message)) + + var component: OneBridgeComponent? = delegate.component() + XCTAssertNotNil(component) + + delegate.onViewDidDisappear() + XCTAssertFalse(delegate.bridgeDidReceiveMessage(message)) + + component = delegate.component() + XCTAssertNil(component) + } + + func testBridgeForwardsViewWillAppearToComponents() { + delegate.bridgeDidReceiveMessage(testMessage()) + + let component: BridgeComponentSpy? = delegate.component() + XCTAssertNotNil(component) + + delegate.onViewWillAppear() + XCTAssertTrue(component!.onViewWillAppearWasCalled) + } + + func testBridgeForwardsViewDidAppearToComponents() { + delegate.bridgeDidReceiveMessage(testMessage()) + + let component: BridgeComponentSpy? = delegate.component() + XCTAssertNotNil(component) + + delegate.onViewDidAppear() + XCTAssertTrue(component!.onViewDidAppearWasCalled) + } + + func testBridgeForwardsViewWillDisappearToComponents() { + delegate.bridgeDidReceiveMessage(testMessage()) + + let component: BridgeComponentSpy? = delegate.component() + XCTAssertNotNil(component) + + delegate.onViewWillDisappear() + XCTAssertTrue(component!.onViewWillDisappearWasCalled) + } + + func testBridgeForwardsViewDidDisappearToComponents() { + delegate.bridgeDidReceiveMessage(testMessage()) + + let component: BridgeComponentSpy? = delegate.component() + XCTAssertNotNil(component) + + delegate.onViewDidDisappear() + XCTAssertTrue(component!.onViewDidDisappearWasCalled) + } + + func testBridgeDestinationIsActiveAfterViewWillDisappearIsCalled() { + delegate.bridgeDidReceiveMessage(testMessage()) + + let component: BridgeComponentSpy? = delegate.component() + XCTAssertNotNil(component) + + delegate.onViewWillDisappear() + XCTAssertTrue(delegate.bridgeDidReceiveMessage(testMessage())) + } + + private func testMessage() -> Message { + return Message(id: "1", + component: "two", + event: "connect", + metadata: .init(url: "https://37signals.com"), + jsonData: json) + } +} + +private class BridgeDestinationSpy: BridgeDestination {} + +private class OneBridgeComponent: BridgeComponent { + static override var name: String { "one" } + + required init(destination: BridgeDestination, delegate: BridgeDelegate) { + super.init(destination: destination, delegate: delegate) + } + + override func handle(message: Strada.Message) {} +} + +private class BridgeComponentSpy: BridgeComponent { + static override var name: String { "two" } + + var handleMessageWasCalled = false + var handleMessageArg: Message? + + var onViewDidLoadWasCalled = false + var onViewWillAppearWasCalled = false + var onViewDidAppearWasCalled = false + var onViewWillDisappearWasCalled = false + var onViewDidDisappearWasCalled = false + + required init(destination: BridgeDestination, delegate: BridgeDelegate) { + super.init(destination: destination, delegate: delegate) + } + + override func handle(message: Strada.Message) { + handleMessageWasCalled = true + handleMessageArg = message + } + + override func onViewDidLoad() { + onViewDidLoadWasCalled = true + } + + override func onViewWillAppear() { + onViewWillAppearWasCalled = true + } + + override func onViewDidAppear() { + onViewDidAppearWasCalled = true + } + + override func onViewWillDisappear() { + onViewWillDisappearWasCalled = true + } + + override func onViewDidDisappear() { + onViewDidDisappearWasCalled = true + } +} + +private class BridgeSpy: Bridgable { + var delegate: Strada.BridgeDelegate? = nil + var webView: WKWebView? = nil + + var registerComponentWasCalled = false + var registerComponentArg: String? = nil + + var registerComponentsWasCalled = false + var registerComponentsArg: [String]? = nil + + var unregisterComponentWasCalled = false + var unregisterComponentArg: String? = nil + + var sendMessageWasCalled = false + var sendMessageArg: Message? = nil + + func register(component: String) { + registerComponentWasCalled = true + registerComponentArg = component + } + + func register(components: [String]) { + registerComponentsWasCalled = true + registerComponentsArg = components + } + + func unregister(component: String) { + unregisterComponentWasCalled = true + unregisterComponentArg = component + } + + func send(_ message: Strada.Message) { + sendMessageWasCalled = true + sendMessageArg = message + } +} diff --git a/Tests/BridgeTests.swift b/Tests/BridgeTests.swift index 2580ba2..50231dc 100644 --- a/Tests/BridgeTests.swift +++ b/Tests/BridgeTests.swift @@ -3,22 +3,24 @@ import WebKit @testable import Strada class BridgeTests: XCTestCase { - func testInitAutomaticallyLoadsIntoWebView() { + func testInitWithANewWebViewAutomaticallyLoadsIntoWebView() { let webView = WKWebView() let userContentController = webView.configuration.userContentController XCTAssertTrue(userContentController.userScripts.isEmpty) - _ = Bridge(webView: webView) + Bridge.initialize(webView) XCTAssertEqual(userContentController.userScripts.count, 1) } - func testLoadIntoConfiguration() { + func testInitWithTheSameWebViewDoesNotLoadTwice() { let webView = WKWebView() let userContentController = webView.configuration.userContentController XCTAssertTrue(userContentController.userScripts.isEmpty) - let bridge = Bridge() - bridge.webView = webView + Bridge.initialize(webView) + XCTAssertEqual(userContentController.userScripts.count, 1) + + Bridge.initialize(webView) XCTAssertEqual(userContentController.userScripts.count, 1) } @@ -79,7 +81,8 @@ class BridgeTests: XCTestCase { } func testEvaluateJavaScriptReturnsErrorForNoWebView() { - let bridge = Bridge() + let bridge = Bridge(webView: WKWebView()) + bridge.webView = nil let expectation = self.expectation(description: "error handler") bridge.evaluate(function: "test", arguments: []) { (result, error) in @@ -87,7 +90,7 @@ class BridgeTests: XCTestCase { expectation.fulfill() } - waitForExpectations(timeout: 0.5) + waitForExpectations(timeout: 2) } func testEvaluateFunction() { @@ -109,7 +112,7 @@ class BridgeTests: XCTestCase { expectation.fulfill() } - waitForExpectations(timeout: 0.5) + waitForExpectations(timeout: 2) } }