Skip to content

Commit

Permalink
Add unit tests for OnBackPressedDispatcher (#1648)
Browse files Browse the repository at this point in the history
  • Loading branch information
squarejesse authored Oct 31, 2023
1 parent 4af0be1 commit d51f1a6
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,11 @@ class EventLog {
"expected no events but was ${received.getOrNull()}"
}
}

/** Discard all events. */
fun clear() {
while (true) {
if (!events.tryReceive().isSuccess) break
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,26 @@ import app.cash.redwood.ui.OnBackPressedCallback
import app.cash.redwood.ui.OnBackPressedDispatcher

internal class FakeOnBackPressedDispatcher : OnBackPressedDispatcher {
private val mutableCallbacks = mutableListOf<OnBackPressedCallback>()

val callbacks: List<OnBackPressedCallback>
get() = mutableCallbacks.toList()

override fun addCallback(onBackPressedCallback: OnBackPressedCallback): Cancellable {
mutableCallbacks += onBackPressedCallback

return object : Cancellable {
override fun cancel() {
mutableCallbacks -= onBackPressedCallback
}
}
}

fun onBack() {
// Only one callback should handle each back press.
val callbackToNotify = mutableCallbacks.lastOrNull {
it.isEnabled
}
callbackToNotify?.handleOnBackPressed()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class FakeZiplineTreehouseUi(
private var nextWidgetId = 1

private lateinit var host: ZiplineTreehouseUi.Host
private val extraServicesToClose = mutableListOf<CancellableService>()

override fun start(host: ZiplineTreehouseUi.Host) {
eventLog += "$name.start()"
Expand All @@ -59,6 +60,22 @@ class FakeZiplineTreehouseUi(
eventLog += "$name.sendEvent($event)"
}

fun addBackHandler(isEnabled: Boolean): CancellableService {
val result = host.addOnBackPressedCallback(object : OnBackPressedCallbackService {
override var isEnabled = isEnabled

override fun handleOnBackPressed() {
eventLog += "$name.onBackPressed()"
}
})

// Keep track of services produced by this ZiplineTreehouseUi so we can close them when this
// service is itself closed. In production code the ZiplineScope does this automatically.
extraServicesToClose += result

return result
}

@Suppress("OVERRIDE_DEPRECATION")
override fun start(
changesSink: ChangesSinkService,
Expand All @@ -79,6 +96,9 @@ class FakeZiplineTreehouseUi(
}

override fun close() {
for (cancellableService in extraServicesToClose) {
cancellableService.close()
}
eventLog += "$name.close()"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ package app.cash.redwood.treehouse

import app.cash.redwood.ui.UiConfiguration
import assertk.assertThat
import assertk.assertions.isEmpty
import assertk.assertions.isEqualTo
import assertk.assertions.isNotEmpty
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -225,6 +227,72 @@ class TreehouseAppContentTest {
eventLog.takeEvent("codeSessionA.app.uis[1].close()")
}

@Test
fun addBackHandler_receives_back_presses_until_canceled() = runTest {
val content = treehouseAppContent()

val view1 = FakeTreehouseView("view1")
content.bind(view1)
codeHost.session = FakeCodeSession("codeSessionA", eventLog)
eventLog.clear()

val backCancelable = codeHost.session!!.appService.uis.single().addBackHandler(true)
view1.onBackPressedDispatcher.onBack()
eventLog.takeEvent("codeSessionA.app.uis[0].onBackPressed()")

view1.onBackPressedDispatcher.onBack()
eventLog.takeEvent("codeSessionA.app.uis[0].onBackPressed()")

backCancelable.cancel()
view1.onBackPressedDispatcher.onBack()
eventLog.assertNoEvents()

content.unbind()
eventLog.takeEvent("codeSessionA.app.uis[0].close()")
}

@Test
fun addBackHandler_receives_no_back_presses_if_disabled() = runTest {
val content = treehouseAppContent()

val view1 = FakeTreehouseView("view1")
content.bind(view1)
codeHost.session = FakeCodeSession("codeSessionA", eventLog)
eventLog.clear()

val backCancelable = codeHost.session!!.appService.uis.single().addBackHandler(false)
view1.onBackPressedDispatcher.onBack()
eventLog.assertNoEvents()

backCancelable.cancel()

content.unbind()
eventLog.takeEvent("codeSessionA.app.uis[0].close()")
}

@Test
fun backHandlers_cleared_when_session_changes() = runTest {
val content = treehouseAppContent()

val view1 = FakeTreehouseView("view1")
content.bind(view1)
val codeSessionA = FakeCodeSession("codeSessionA", eventLog)
codeHost.session = codeSessionA

codeSessionA.appService.uis.single().addBackHandler(true)
assertThat(view1.onBackPressedDispatcher.callbacks).isNotEmpty()

val codeSessionB = FakeCodeSession("codeSessionB", eventLog)
codeHost.session = codeSessionB

// When we close codeSessionA, its back handlers are released with it.
assertThat(view1.onBackPressedDispatcher.callbacks).isEmpty()
eventLog.clear()

content.unbind()
eventLog.takeEvent("codeSessionB.app.uis[0].close()")
}

private fun TestScope.treehouseAppContent(): TreehouseAppContent<FakeAppService> {
return TreehouseAppContent(
codeHost = codeHost,
Expand Down

0 comments on commit d51f1a6

Please sign in to comment.