Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose didReceive(message:) and other component's functions #17

Merged
merged 4 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 50 additions & 20 deletions Source/BridgeComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ 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()
func onViewWillAppear()
func onViewDidAppear()
func onViewWillDisappear()
func onViewDidDisappear()

func didReceive(message: Message)
func viewDidLoad()
func viewWillAppear()
func viewDidAppear()
func viewWillDisappear()
func viewDidDisappear()
}

open class BridgeComponent: BridgingComponent {
Expand All @@ -25,12 +32,19 @@ 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
}

/// 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`.
///
Expand All @@ -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`.
///
Expand Down Expand Up @@ -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.
Expand All @@ -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()
}

Expand Down
29 changes: 25 additions & 4 deletions Source/BridgeDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: BridgeComponent>() -> 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? {
Expand Down Expand Up @@ -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))")
Expand Down
20 changes: 20 additions & 0 deletions Strada.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
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 */; };
Expand Down Expand Up @@ -67,6 +70,9 @@
E20978482A71366B00CDEEE5 /* Data+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utils.swift"; sourceTree = "<group>"; };
E209784A2A714D4E00CDEEE5 /* String+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+JSON.swift"; sourceTree = "<group>"; };
E209784C2A714F1900CDEEE5 /* Dictionary+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+JSON.swift"; sourceTree = "<group>"; };
E227FAED2A94B35900A645E4 /* BridgeDelegateSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeDelegateSpy.swift; sourceTree = "<group>"; };
E227FAEF2A94D34E00A645E4 /* ComposerComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerComponent.swift; sourceTree = "<group>"; };
E227FAF22A94D57300A645E4 /* ComposerComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerComponentTests.swift; sourceTree = "<group>"; };
E22CBEFE2A84D7060024EFB8 /* StradaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StradaConfig.swift; sourceTree = "<group>"; };
E22CBF002A84DC380024EFB8 /* Strada.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strada.swift; sourceTree = "<group>"; };
E22CBF022A852A140024EFB8 /* UserAgentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -140,6 +146,7 @@
9274F1F22229963B003E85F4 /* Tests */ = {
isa = PBXGroup;
children = (
E227FAF12A94D48C00A645E4 /* ComponentTestExample */,
E2FDCF9C2A829C6F003D27AE /* TestData.swift */,
E2FDCF992A829AD5003D27AE /* Spies */,
C1EB052D2588201600933244 /* BridgeTests.swift */,
Expand All @@ -165,11 +172,21 @@
path = Extensions;
sourceTree = "<group>";
};
E227FAF12A94D48C00A645E4 /* ComponentTestExample */ = {
isa = PBXGroup;
children = (
E227FAEF2A94D34E00A645E4 /* ComposerComponent.swift */,
E227FAF22A94D57300A645E4 /* ComposerComponentTests.swift */,
);
path = ComponentTestExample;
sourceTree = "<group>";
};
E2FDCF992A829AD5003D27AE /* Spies */ = {
isa = PBXGroup;
children = (
E2FDCF9A2A829AEE003D27AE /* BridgeSpy.swift */,
E2FDCF9E2A829CA0003D27AE /* BridgeComponentSpy.swift */,
E227FAED2A94B35900A645E4 /* BridgeDelegateSpy.swift */,
);
path = Spies;
sourceTree = "<group>";
Expand Down Expand Up @@ -309,7 +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 */,
Expand Down
77 changes: 20 additions & 57 deletions Tests/BridgeComponentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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() {
Expand All @@ -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() {
Expand All @@ -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() {
Expand All @@ -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)
}
}
Loading