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()) + } +}