Skip to content

Commit

Permalink
Merge pull request #20 from hotwired/store-event-messages
Browse files Browse the repository at this point in the history
Store received messages within components
  • Loading branch information
jayohms authored Aug 8, 2023
2 parents 196648d + 6ece56d commit b9e41a7
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 100 deletions.
90 changes: 85 additions & 5 deletions strada/src/main/kotlin/dev/hotwire/strada/BridgeComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,94 @@ abstract class BridgeComponent<in D : BridgeDestination>(
val name: String,
private val delegate: BridgeDelegate<D>
) {
abstract fun onReceive(message: Message)
private val receivedMessages = hashMapOf<String, Message>()

fun replyWith(message: Message) {
internal fun didReceive(message: Message) {
receivedMessages[message.event] = message
onReceive(message)
}

internal fun didStart() {
onStart()
}

internal fun didStop() {
onStop()
}

/**
* Called when the component's destination starts (and is active)
* based on its lifecycle events. You can use this as an opportunity
* to update the component's state/view.
*/
protected open fun onStart() {}

/**
* Called when the component's destination stops (and is inactive)
* based on its lifecycle events. You can use this as an opportunity
* to update the component's state/view.
*/
protected open fun onStop() {}

/**
* Called when a message is received from the web bridge. Handle the
* message for its `event` type for the custom component's behavior.
*/
protected abstract fun onReceive(message: Message)

/**
* Reply to the web with a received message, optionally replacing its
* `event` or `jsonData`.
*/
protected fun replyWith(message: Message): Boolean {
return reply(message)
}

/**
* Reply to the web with the last received message for a given `event`
* with its original `jsonData`.
*
* NOTE: If a message has not been received for the given `event`, the
* reply will be ignored.
*/
protected fun replyTo(event: String): Boolean {
val message = receivedMessageFor(event) ?: run {
logEvent("bridgeMessageFailedToReply", "message for event '$event' was not received")
return false
}

return reply(message)
}

/**
* Reply to the web with the last received message for a given `event`,
* replacing its `jsonData`.
*
* NOTE: If a message has not been received for the given `event`, the
* reply will be ignored.
*/
protected fun replyTo(event: String, jsonData: String): Boolean {
val message = receivedMessageFor(event) ?: run {
logEvent("bridgeMessageFailedToReply", "message for event '$event' was not received")
return false
}

return reply(message.replacing(jsonData = jsonData))
}

/**
* Returns the last received message for a given `event`, if available.
*/
protected fun receivedMessageFor(event: String): Message? {
return receivedMessages[event]
}

private fun reply(message: Message): Boolean {
delegate.bridge?.replyWith(message) ?: run {
logEvent("bridgeMessageFailedToReply", "bridge is not available")
return false
}
}

open fun onStart() {}
open fun onStop() {}
return true
}
}
6 changes: 3 additions & 3 deletions strada/src/main/kotlin/dev/hotwire/strada/BridgeDelegate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class BridgeDelegate<D : BridgeDestination>(
internal fun bridgeDidReceiveMessage(message: Message): Boolean {
return if (destinationIsActive && location == message.metadata?.url) {
logMessage("bridgeDidReceiveMessage", message)
getOrCreateComponent(message.component)?.onReceive(message)
getOrCreateComponent(message.component)?.didReceive(message)
true
} else {
logMessage("bridgeDidIgnoreMessage", message)
Expand All @@ -68,11 +68,11 @@ class BridgeDelegate<D : BridgeDestination>(
override fun onStart(owner: LifecycleOwner) {
logEvent("bridgeDestinationDidStart", location)
destinationIsActive = true
activeComponents.forEach { it.onStart() }
activeComponents.forEach { it.didStart() }
}

override fun onStop(owner: LifecycleOwner) {
activeComponents.forEach { it.onStop() }
activeComponents.forEach { it.didStop() }
destinationIsActive = false
logEvent("bridgeDestinationDidStop", location)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,15 @@ import org.junit.Test
class BridgeComponentFactoryTest {
@Test
fun createComponents() {
val factories = listOf(
BridgeComponentFactory("one", ::OneBridgeComponent),
BridgeComponentFactory("two", ::TwoBridgeComponent)
)

val delegate = BridgeDelegate(
location = "https://37signals.com",
destination = AppBridgeDestination(),
componentFactories = factories
)
val factories = TestData.componentFactories
val delegate = TestData.bridgeDelegate

val componentOne = factories[0].create(delegate)
assertEquals("one", componentOne.name)
assertTrue(componentOne is OneBridgeComponent)
assertTrue(componentOne is TestData.OneBridgeComponent)

val componentTwo = factories[1].create(delegate)
assertEquals("two", componentTwo.name)
assertTrue(componentTwo is TwoBridgeComponent)
}

class AppBridgeDestination : BridgeDestination {
override fun bridgeWebViewIsReady() = true
}

private abstract class AppBridgeComponent(
name: String,
delegate: BridgeDelegate<AppBridgeDestination>
) : BridgeComponent<AppBridgeDestination>(name, delegate)

private class OneBridgeComponent(
name: String,
delegate: BridgeDelegate<AppBridgeDestination>
) : AppBridgeComponent(name, delegate) {
override fun onReceive(message: Message) {}
}

private class TwoBridgeComponent(
name: String,
delegate: BridgeDelegate<AppBridgeDestination>
) : AppBridgeComponent(name, delegate) {
override fun onReceive(message: Message) {}
assertTrue(componentTwo is TestData.TwoBridgeComponent)
}
}
110 changes: 110 additions & 0 deletions strada/src/test/kotlin/dev/hotwire/strada/BridgeComponentTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package dev.hotwire.strada

import com.nhaarman.mockito_kotlin.*
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test

class BridgeComponentTest {
private lateinit var component: TestData.OneBridgeComponent
private val delegate: BridgeDelegate<TestData.AppBridgeDestination> = mock()
private val bridge: Bridge = mock()

private val message = Message(
id = "1",
component = "one",
event = "connect",
metadata = Metadata("https://37signals.com"),
jsonData = """{"title":"Page-title","subtitle":"Page-subtitle"}"""
)

@Before
fun setup() {
component = TestData.OneBridgeComponent("one", delegate)
whenever(delegate.bridge).thenReturn(bridge)
}

@Test
fun didReceive() {
assertNull(component.receivedMessageForPublic("connect"))

component.didReceive(message)
assertEquals(message, component.receivedMessageForPublic("connect"))
}

@Test
fun didStart() {
assertEquals(false, component.onStartCalled)

component.didStart()
assertEquals(true, component.onStartCalled)
}

@Test
fun didStop() {
assertEquals(false, component.onStopCalled)

component.didStop()
assertEquals(true, component.onStopCalled)
}

@Test
fun didReceiveSavesLastMessage() {
val newJsonData = """{"title":"Page-title"}"""
val newMessage = message.replacing(jsonData = newJsonData)

component.didReceive(message)
assertEquals(message, component.receivedMessageForPublic("connect"))

component.didReceive(newMessage)
assertEquals(newMessage, component.receivedMessageForPublic("connect"))
}

@Test
fun replyWith() {
val newJsonData = """{"title":"Page-title"}"""
val newMessage = message.replacing(jsonData = newJsonData)

val replied = component.replyWithPublic(newMessage)
assertEquals(true, replied)
verify(bridge).replyWith(eq(newMessage))
}

@Test
fun replyTo() {
component.didReceive(message)

val replied = component.replyToPublic("connect")
assertEquals(true, replied)
verify(bridge).replyWith(eq(message))
}

@Test
fun replyToReplacingData() {
val newJsonData = """{"title":"Page-title"}"""
val newMessage = message.replacing(jsonData = newJsonData)

component.didReceive(message)

val replied = component.replyToPublic("connect", newJsonData)
assertEquals(true, replied)
verify(bridge).replyWith(eq(newMessage))
}

@Test
fun replyToIgnoresNotReceived() {
val replied = component.replyToPublic("connect")
assertEquals(false, replied)
verify(bridge, never()).replyWith(any())
}

@Test
fun replyToReplacingDataIgnoresNotReceived() {
val newJsonData = """{"title":"Page-title"}"""

val replied = component.replyToPublic("connect", newJsonData)
assertEquals(false, replied)
verify(bridge, never()).replyWith(any())
}
}
39 changes: 8 additions & 31 deletions strada/src/test/kotlin/dev/hotwire/strada/BridgeDelegateTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import org.junit.Test
import org.mockito.Mockito.verify

class BridgeDelegateTest {
private lateinit var delegate: BridgeDelegate<AppBridgeDestination>
private lateinit var delegate: BridgeDelegate<TestData.AppBridgeDestination>
private lateinit var lifecycleOwner: TestLifecycleOwner
private val bridge: Bridge = mock()
private val webView: WebView = mock()

private val factories = listOf(
BridgeComponentFactory("one", ::OneBridgeComponent),
BridgeComponentFactory("two", ::TwoBridgeComponent)
BridgeComponentFactory("one", TestData::OneBridgeComponent),
BridgeComponentFactory("two", TestData::TwoBridgeComponent)
)

@Rule
Expand All @@ -35,7 +35,7 @@ class BridgeDelegateTest {

delegate = BridgeDelegate(
location = "https://37signals.com",
destination = AppBridgeDestination(),
destination = TestData.AppBridgeDestination(),
componentFactories = factories
)
delegate.bridge = bridge
Expand Down Expand Up @@ -72,9 +72,9 @@ class BridgeDelegateTest {
jsonData = """{"title":"Page-title","subtitle":"Page-subtitle"}"""
)

assertNull(delegate.component<OneBridgeComponent>())
assertNull(delegate.component<TestData.OneBridgeComponent>())
assertEquals(true, delegate.bridgeDidReceiveMessage(message))
assertNotNull(delegate.component<OneBridgeComponent>())
assertNotNull(delegate.component<TestData.OneBridgeComponent>())
}

@Test
Expand Down Expand Up @@ -133,33 +133,10 @@ class BridgeDelegateTest {
)

assertEquals(true, delegate.bridgeDidReceiveMessage(message))
assertNotNull(delegate.component<OneBridgeComponent>())
assertNotNull(delegate.component<TestData.OneBridgeComponent>())

lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
assertEquals(false, delegate.bridgeDidReceiveMessage(message))
assertNull(delegate.component<OneBridgeComponent>())
}

class AppBridgeDestination : BridgeDestination {
override fun bridgeWebViewIsReady() = true
}

private abstract class AppBridgeComponent(
name: String,
delegate: BridgeDelegate<AppBridgeDestination>
) : BridgeComponent<AppBridgeDestination>(name, delegate)

private class OneBridgeComponent(
name: String,
delegate: BridgeDelegate<AppBridgeDestination>
) : AppBridgeComponent(name, delegate) {
override fun onReceive(message: Message) {}
}

private class TwoBridgeComponent(
name: String,
delegate: BridgeDelegate<AppBridgeDestination>
) : AppBridgeComponent(name, delegate) {
override fun onReceive(message: Message) {}
assertNull(delegate.component<TestData.OneBridgeComponent>())
}
}
Loading

0 comments on commit b9e41a7

Please sign in to comment.