From f76570aa424cb4297758e4643c4ea931a5d0608b Mon Sep 17 00:00:00 2001 From: Ray Ryan Date: Fri, 9 May 2025 15:20:30 -0700 Subject: [PATCH] Punches up composite rendering API and demonstrates nav logging. Introduces a few interfaces to make it easier to navigate composite rendering structures, useful for both production logging and workflow unit testing. Updates some samples (hellobackbutton, ravenapp and poetryapp) to demonstrate their use for navigation logging. --- .../sample/poetryapp/PoetryActivity.kt | 10 +- .../squareup/sample/ravenapp/RavenActivity.kt | 8 +- .../overviewdetail/OverviewDetailScreen.kt | 9 +- .../hellobackbutton/AreYouSureWorkflow.kt | 14 ++- .../HelloBackButtonActivity.kt | 15 ++- workflow-ui/core-android/api/core-android.api | 1 + workflow-ui/core-common/api/core-common.api | 32 +++++- .../com/squareup/workflow1/ui/Container.kt | 56 +++++++--- .../ui/navigation/BodyAndOverlaysScreen.kt | 5 +- .../squareup/workflow1/ui/ContainerTest.kt | 105 ++++++++++++++++++ 10 files changed, 228 insertions(+), 27 deletions(-) create mode 100644 workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/ContainerTest.kt diff --git a/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt index c860b59ffe..9879be6246 100644 --- a/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt +++ b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt @@ -17,9 +17,11 @@ import com.squareup.workflow1.config.AndroidRuntimeConfigTools import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowLayout import com.squareup.workflow1.ui.renderWorkflowIn +import com.squareup.workflow1.ui.unwrap import com.squareup.workflow1.ui.withRegistry -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import timber.log.Timber private val viewRegistry = SampleContainers @@ -44,13 +46,15 @@ class PoetryActivity : AppCompatActivity() { } class PoetryModel(savedState: SavedStateHandle) : ViewModel() { - val renderings: StateFlow by lazy { + val renderings: Flow by lazy { renderWorkflowIn( workflow = RealPoemsBrowserWorkflow(RealPoemWorkflow()), scope = viewModelScope, prop = 0 to 0 to Poem.allPoems, savedStateHandle = savedState, runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig() - ) + ).onEach { + Timber.i("Navigated to %s", it.unwrap()) + } } } diff --git a/samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt b/samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt index 101ab4671a..65c65d70bc 100644 --- a/samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt +++ b/samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt @@ -17,10 +17,12 @@ import com.squareup.workflow1.config.AndroidRuntimeConfigTools import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowLayout import com.squareup.workflow1.ui.renderWorkflowIn +import com.squareup.workflow1.ui.unwrap import com.squareup.workflow1.ui.withRegistry import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber @@ -53,7 +55,7 @@ class RavenActivity : AppCompatActivity() { class RavenModel(savedState: SavedStateHandle) : ViewModel() { private val running = Job() - val renderings: StateFlow by lazy { + val renderings: Flow by lazy { renderWorkflowIn( workflow = RealPoemWorkflow(), scope = viewModelScope, @@ -62,6 +64,8 @@ class RavenModel(savedState: SavedStateHandle) : ViewModel() { runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig() ) { running.complete() + }.onEach { + Timber.i("Navigated to %s", it.unwrap()) } } diff --git a/samples/containers/common/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreen.kt b/samples/containers/common/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreen.kt index a963295dd5..919fe2b11b 100644 --- a/samples/containers/common/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreen.kt +++ b/samples/containers/common/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreen.kt @@ -1,6 +1,7 @@ package com.squareup.sample.container.overviewdetail import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.Unwrappable import com.squareup.workflow1.ui.navigation.BackStackScreen import com.squareup.workflow1.ui.navigation.plus @@ -19,7 +20,7 @@ class OverviewDetailScreen private constructor( val overviewRendering: BackStackScreen, val detailRendering: BackStackScreen? = null, val selectDefault: (() -> Unit)? = null -) : Screen { +) : Screen, Unwrappable { constructor( overviewRendering: BackStackScreen, detailRendering: BackStackScreen @@ -37,6 +38,12 @@ class OverviewDetailScreen private constructor( operator fun component1(): BackStackScreen = overviewRendering operator fun component2(): BackStackScreen? = detailRendering + /** + * For nicer logging. See the call to [unwrap][com.squareup.workflow1.ui.unwrap] + * in the activity. + */ + override val unwrapped = detailRendering ?: overviewRendering + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt index 0c1a5603e2..685d5feead 100644 --- a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt +++ b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt @@ -14,6 +14,7 @@ import com.squareup.workflow1.ui.AndroidScreen import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ScreenViewFactory.Companion.map +import com.squareup.workflow1.ui.Unwrappable import com.squareup.workflow1.ui.navigation.AlertOverlay import com.squareup.workflow1.ui.navigation.AlertOverlay.Button.NEGATIVE import com.squareup.workflow1.ui.navigation.AlertOverlay.Button.POSITIVE @@ -37,12 +38,19 @@ object AreYouSureWorkflow : ): State = snapshot?.toParcelable() ?: Running class Rendering( - val base: Screen, - val alert: AlertOverlay? = null - ) : AndroidScreen { + private val base: Screen, + private val alert: AlertOverlay? = null + ) : AndroidScreen, Unwrappable { override val viewFactory: ScreenViewFactory = map { newRendering -> BodyAndOverlaysScreen(newRendering.base, listOfNotNull(newRendering.alert)) } + + /** + * For nicer logging. See the call to [unwrap][com.squareup.workflow1.ui.unwrap] + * in [HelloBackButtonActivity]. + */ + override val unwrapped: Any + get() = alert ?: base } @Parcelize diff --git a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt index 6c492a548f..936a51eb14 100644 --- a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt +++ b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt @@ -15,11 +15,14 @@ import com.squareup.workflow1.config.AndroidRuntimeConfigTools import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowLayout import com.squareup.workflow1.ui.renderWorkflowIn +import com.squareup.workflow1.ui.unwrap import com.squareup.workflow1.ui.withRegistry import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import timber.log.Timber private val viewRegistry = SampleContainers @@ -39,12 +42,18 @@ class HelloBackButtonActivity : AppCompatActivity() { finish() } } + + companion object { + init { + Timber.plant(Timber.DebugTree()) + } + } } class HelloBackButtonModel(savedState: SavedStateHandle) : ViewModel() { private val running = Job() - val renderings: StateFlow by lazy { + val renderings: Flow by lazy { renderWorkflowIn( workflow = AreYouSureWorkflow, scope = viewModelScope, @@ -54,6 +63,8 @@ class HelloBackButtonModel(savedState: SavedStateHandle) : ViewModel() { // This workflow handles the back button itself, so the activity can't. // Instead, the workflow emits an output to signal that it's time to shut things down. running.complete() + }.onEach { + Timber.i("Navigated to %s", it.unwrap()) } } diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index 6e43b8975c..a9530a8e18 100644 --- a/workflow-ui/core-android/api/core-android.api +++ b/workflow-ui/core-android/api/core-android.api @@ -226,6 +226,7 @@ public final class com/squareup/workflow1/ui/navigation/BackButtonScreen : com/s public synthetic fun getContent ()Ljava/lang/Object; public final fun getOnBackPressed ()Lkotlin/jvm/functions/Function0; public final fun getShadow ()Z + public fun getUnwrapped ()Ljava/lang/Object; public fun getViewFactory ()Lcom/squareup/workflow1/ui/ScreenViewFactory; public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper; diff --git a/workflow-ui/core-common/api/core-common.api b/workflow-ui/core-common/api/core-common.api index 7ea6749533..49e536fb37 100644 --- a/workflow-ui/core-common/api/core-common.api +++ b/workflow-ui/core-common/api/core-common.api @@ -12,11 +12,27 @@ public final class com/squareup/workflow1/ui/CompatibleKt { public static final fun compatible (Ljava/lang/Object;Ljava/lang/Object;)Z } -public abstract interface class com/squareup/workflow1/ui/Container { +public abstract interface class com/squareup/workflow1/ui/Composite : com/squareup/workflow1/ui/Unwrappable { public abstract fun asSequence ()Lkotlin/sequences/Sequence; + public abstract fun getUnwrapped ()Ljava/lang/Object; +} + +public final class com/squareup/workflow1/ui/Composite$DefaultImpls { + public static fun getUnwrapped (Lcom/squareup/workflow1/ui/Composite;)Ljava/lang/Object; +} + +public abstract interface class com/squareup/workflow1/ui/Container : com/squareup/workflow1/ui/Composite { public abstract fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; } +public final class com/squareup/workflow1/ui/Container$DefaultImpls { + public static fun getUnwrapped (Lcom/squareup/workflow1/ui/Container;)Ljava/lang/Object; +} + +public final class com/squareup/workflow1/ui/ContainerKt { + public static final fun unwrap (Ljava/lang/Object;)Ljava/lang/Object; +} + public final class com/squareup/workflow1/ui/EnvironmentScreen : com/squareup/workflow1/ui/Screen, com/squareup/workflow1/ui/Wrapper { public fun (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V public synthetic fun (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -25,6 +41,7 @@ public final class com/squareup/workflow1/ui/EnvironmentScreen : com/squareup/wo public fun getContent ()Lcom/squareup/workflow1/ui/Screen; public synthetic fun getContent ()Ljava/lang/Object; public final fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment; + public fun getUnwrapped ()Ljava/lang/Object; public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/EnvironmentScreen; public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper; @@ -49,6 +66,7 @@ public final class com/squareup/workflow1/ui/NamedScreen : com/squareup/workflow public fun getContent ()Lcom/squareup/workflow1/ui/Screen; public synthetic fun getContent ()Ljava/lang/Object; public final fun getName ()Ljava/lang/String; + public fun getUnwrapped ()Ljava/lang/Object; public fun hashCode ()I public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/NamedScreen; @@ -70,6 +88,10 @@ public final class com/squareup/workflow1/ui/TextControllerKt { public static synthetic fun TextController$default (Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/TextController; } +public abstract interface class com/squareup/workflow1/ui/Unwrappable { + public abstract fun getUnwrapped ()Ljava/lang/Object; +} + public final class com/squareup/workflow1/ui/ViewEnvironment { public static final field Companion Lcom/squareup/workflow1/ui/ViewEnvironment$Companion; public fun equals (Ljava/lang/Object;)Z @@ -138,6 +160,7 @@ public abstract interface class com/squareup/workflow1/ui/Wrapper : com/squareup public final class com/squareup/workflow1/ui/Wrapper$DefaultImpls { public static fun asSequence (Lcom/squareup/workflow1/ui/Wrapper;)Lkotlin/sequences/Sequence; public static fun getCompatibilityKey (Lcom/squareup/workflow1/ui/Wrapper;)Ljava/lang/String; + public static fun getUnwrapped (Lcom/squareup/workflow1/ui/Wrapper;)Ljava/lang/Object; } public final class com/squareup/workflow1/ui/navigation/AlertOverlay : com/squareup/workflow1/ui/navigation/ModalOverlay { @@ -218,6 +241,7 @@ public final class com/squareup/workflow1/ui/navigation/BackStackScreen : com/sq public final fun getFrames ()Ljava/util/List; public final fun getName ()Ljava/lang/String; public final fun getTop ()Lcom/squareup/workflow1/ui/Screen; + public fun getUnwrapped ()Ljava/lang/Object; public fun hashCode ()I public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/navigation/BackStackScreen; @@ -241,13 +265,15 @@ public final class com/squareup/workflow1/ui/navigation/BackStackScreenKt { public static synthetic fun toBackStackScreenOrNull$default (Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/navigation/BackStackScreen; } -public final class com/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen : com/squareup/workflow1/ui/Compatible, com/squareup/workflow1/ui/Screen { +public final class com/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen : com/squareup/workflow1/ui/Compatible, com/squareup/workflow1/ui/Composite, com/squareup/workflow1/ui/Screen { public fun (Lcom/squareup/workflow1/ui/Screen;Ljava/util/List;Ljava/lang/String;)V public synthetic fun (Lcom/squareup/workflow1/ui/Screen;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun asSequence ()Lkotlin/sequences/Sequence; public final fun getBody ()Lcom/squareup/workflow1/ui/Screen; public fun getCompatibilityKey ()Ljava/lang/String; public final fun getName ()Ljava/lang/String; public final fun getOverlays ()Ljava/util/List; + public fun getUnwrapped ()Ljava/lang/Object; public final fun mapBody (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen; public final fun mapOverlays (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen; } @@ -258,6 +284,7 @@ public final class com/squareup/workflow1/ui/navigation/FullScreenModal : com/sq public fun getCompatibilityKey ()Ljava/lang/String; public fun getContent ()Lcom/squareup/workflow1/ui/Screen; public synthetic fun getContent ()Ljava/lang/Object; + public fun getUnwrapped ()Ljava/lang/Object; public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper; public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/navigation/FullScreenModal; @@ -278,5 +305,6 @@ public abstract interface class com/squareup/workflow1/ui/navigation/ScreenOverl public final class com/squareup/workflow1/ui/navigation/ScreenOverlay$DefaultImpls { public static fun asSequence (Lcom/squareup/workflow1/ui/navigation/ScreenOverlay;)Lkotlin/sequences/Sequence; public static fun getCompatibilityKey (Lcom/squareup/workflow1/ui/navigation/ScreenOverlay;)Ljava/lang/String; + public static fun getUnwrapped (Lcom/squareup/workflow1/ui/navigation/ScreenOverlay;)Ljava/lang/Object; } diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Container.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Container.kt index 52a5d4e091..619059e7e1 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Container.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Container.kt @@ -3,25 +3,55 @@ package com.squareup.workflow1.ui import com.squareup.workflow1.ui.Compatible.Companion.keyFor /** - * A rendering type comprised of a set of other renderings. + * A rendering that wraps another that is actually the interesting + * bit (read: the visible bit), particularly from a logging or testing + * point of view. * - * Why two parameter types? The separate [BaseT] type allows implementations + * This is the easiest way to customize behavior of the [unwrap] function. + */ +public interface Unwrappable { + /** Topmost wrapped content, or `this` if empty. */ + public val unwrapped: Any +} + +/** + * Handy for logging and testing, extracts the "topmost" bit from a receiving + * workflow rendering, honoring [Unwrappable] if applicable. + */ +public tailrec fun Any.unwrap(): Any { + if (this !is Unwrappable) return this + return unwrapped.unwrap() +} + +/** + * A rendering that can be decomposed to a [sequence][asSequence] of others. + */ +public interface Composite : Unwrappable { + public fun asSequence(): Sequence + + public override val unwrapped: Any get() = asSequence().lastOrNull() ?: this +} + +/** + * A structured [Composite] rendering comprised of a set of other + * renderings of a [specific type][C] of a particular [category][CategoryT], + * and whose contents can be transformed by [map]. + * + * Why two parameter types? The separate [CategoryT] type allows implementations * and sub-interfaces to constrain the types that [map] is allowed to - * transform [C] to. E.g., it allows `FooWrapper` to declare + * transform [C] to. E.g., it allows `BunchOfScreens` to declare * that [map] is only able to transform `S` to other types of `Screen`. * - * @param BaseT the invariant base type of the contents of such a container, + * @param CategoryT the invariant base type of the contents of such a container, * usually [Screen] or [Overlay][com.squareup.workflow1.ui.navigation.Overlay]. - * It is common for the [Container] itself to implement [BaseT], but that is + * It is common for the [Container] itself to implement [CategoryT], but that is * not a requirement. E.g., [ScreenOverlay][com.squareup.workflow1.ui.navigation.ScreenOverlay] * is an [Overlay][com.squareup.workflow1.ui.navigation.Overlay], but it * wraps a [Screen]. * - * @param C the specific subtype of [BaseT] collected by this [Container]. + * @param C the specific subtype of [CategoryT] collected by this [Container]. */ -public interface Container { - public fun asSequence(): Sequence - +public interface Container : Composite { /** * Returns a [Container] with the [transform]ed contents of the receiver. * It is expected that an implementation will take advantage of covariance @@ -41,7 +71,7 @@ public interface Container { * val childBackStackScreen = renderChild(childWorkflow) { ... } * val loggingBackStackScreen = childBackStackScreen.map { LoggingScreen(it) } */ - public fun map(transform: (C) -> D): Container + public fun map(transform: (C) -> D): Container } /** @@ -50,9 +80,9 @@ public interface Container { * [EnvironmentScreen][com.squareup.workflow1.ui.EnvironmentScreen] that allows * changes to be made to the [ViewEnvironment]. * - * Usually a [Wrapper] is [Compatible] only with others of the same type with - * [Compatible] [content]. In aid of that, this interface extends [Compatible] and - * provides a convenient default implementation of [compatibilityKey]. + * Usually a [Wrapper] is [Compatible] only with others that are of the same type + * and which are holding [Compatible] [content]. In aid of that, this interface extends + * [Compatible] and provides a convenient default implementation of [compatibilityKey]. */ public interface Wrapper : Container, Compatible { public val content: C diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen.kt index 5b984438f0..73c9f0c83c 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen.kt @@ -2,6 +2,7 @@ package com.squareup.workflow1.ui.navigation import com.squareup.workflow1.ui.Compatible import com.squareup.workflow1.ui.Compatible.Companion.keyFor +import com.squareup.workflow1.ui.Composite import com.squareup.workflow1.ui.Screen /** @@ -75,9 +76,11 @@ public class BodyAndOverlaysScreen( public val body: B, public val overlays: List = emptyList(), public val name: String = "" -) : Screen, Compatible { +) : Screen, Compatible, Composite { override val compatibilityKey: String = keyFor(this, name) + override fun asSequence(): Sequence = sequenceOf(body) + overlays.asSequence() + public fun mapBody(transform: (B) -> S): BodyAndOverlaysScreen { return BodyAndOverlaysScreen(transform(body), overlays, name) } diff --git a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/ContainerTest.kt b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/ContainerTest.kt new file mode 100644 index 0000000000..e6642e59bc --- /dev/null +++ b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/ContainerTest.kt @@ -0,0 +1,105 @@ +package com.squareup.workflow1.ui + +import com.squareup.workflow1.ui.navigation.BackStackScreen +import com.squareup.workflow1.ui.navigation.BodyAndOverlaysScreen +import com.squareup.workflow1.ui.navigation.Overlay +import com.squareup.workflow1.ui.navigation.ScreenOverlay +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertSame + +class ContainerTest { + private data class TestScreen(val id: Int = 0) : Screen + + private data class TestScreenContainer( + val children: List + ) : Screen, Container { + override fun asSequence(): Sequence = children.asSequence() + + override fun map(transform: (T) -> D) = + TestScreenContainer(children.map(transform)) + } + + private data class TestOverlay(val id: Int = 0) : Overlay + + private data class TestScreenOverlay( + override val content: S + ) : ScreenOverlay { + override fun map( + transform: (S) -> ContentU + ) = TestScreenOverlay(transform(content)) + } + + @Test + fun `unwrap returns this`() { + val screen = TestScreen() + assertSame(screen, screen.unwrap()) + } + + @Test + fun `unwrap returns last`() { + assertEquals( + TestScreen(2), + TestScreenContainer(listOf(TestScreen(0), TestScreen(1), TestScreen(2))).unwrap() + ) + } + + @Test + fun `unwrap returns deepest content from nested wrappers`() { + val container = TestScreenContainer( + listOf( + TestScreen(0), + TestScreen(1), + TestScreenContainer( + listOf( + TestScreen(2), + TestScreen(3), + TestScreenContainer(listOf(TestScreen(4), TestScreen(5))) + ) + ), + ) + ) + assertEquals(TestScreen(5), container.unwrap()) + } + + @Test + fun `unwrap prefers outer last`() { + val container = TestScreenContainer( + listOf( + TestScreen(0), + TestScreenContainer(listOf(TestScreen(1), TestScreen(2), TestScreen(3))), + TestScreen(4), + ) + ) + assertEquals(TestScreen(4), container.unwrap()) + } + + @Test fun `can unwrap through BodyAndOverlaysScreen to Body`() { + val container = BodyAndOverlaysScreen(body = TestScreen(), overlays = emptyList()) + assertEquals(TestScreen(), container.unwrap()) + } + + @Test + fun `can unwrap through BodyAndOverlaysScreen to an Overlay`() { + val container = BodyAndOverlaysScreen( + body = TestScreen(), + overlays = listOf(TestOverlay(0), TestOverlay(1)) + ) + + assertEquals(TestOverlay(1), container.unwrap()) + } + + @Test + fun `can unwrap through BodyAndOverlaysScreen through ScreenOverlay and then some`() { + val container = BodyAndOverlaysScreen( + body = TestScreen(), + overlays = listOf( + TestOverlay(0), + TestOverlay(1), + TestScreenOverlay(BackStackScreen(TestScreen(0), TestScreen(1))) + ) + ) + + assertEquals(TestScreen(1), container.unwrap()) + } +}