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

Creating SingleTop operation in 2.x branch #702

Open
wants to merge 1 commit into
base: 2.x
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.bumble.appyx.components.backstack.operation

import androidx.compose.animation.core.AnimationSpec
import com.bumble.appyx.components.backstack.BackStack
import com.bumble.appyx.components.backstack.BackStackModel
import com.bumble.appyx.interactions.model.asElement
import com.bumble.appyx.interactions.model.transition.BaseOperation
import com.bumble.appyx.interactions.model.transition.Operation
import com.bumble.appyx.utils.multiplatform.Parcelize
import com.bumble.appyx.utils.multiplatform.RawValue

/**
* Operation:
*
* [A, B, C, D] + SingleTop(B) = [A, B] // same in stashed, acts as n * Pop
* [A, B, C, D] + SingleTop(D) = [A, B, C, D] // same active, no op
* [A, B, C, D] + SingleTop(E) = [A, B, C, D, E] // not found, acts as Push
*/
@Parcelize
class SingleTop<NavTarget : Any>(
private val navTarget: @RawValue NavTarget,
override var mode: Operation.Mode = Operation.Mode.KEYFRAME
) : BaseOperation<BackStackModel.State<NavTarget>>() {

override fun isApplicable(state: BackStackModel.State<NavTarget>): Boolean = true

override fun createFromState(baseLineState: BackStackModel.State<NavTarget>): BackStackModel.State<NavTarget> =
if (baseLineState.active.interactionTarget == navTarget || baseLineState.stashed.any { it.interactionTarget == navTarget }) {
baseLineState
} else {
baseLineState.copy(created = baseLineState.created + navTarget.asElement())
}

override fun createTargetState(fromState: BackStackModel.State<NavTarget>): BackStackModel.State<NavTarget> {
val elements = fromState.stashed + fromState.active + fromState.created
val trailing = elements.takeLastWhile { it.interactionTarget != navTarget }
val active = elements[elements.size - trailing.size - 1]
return fromState.copy(
active = active,
stashed = elements.take(elements.size - trailing.size - 1),
destroyed = fromState.destroyed + trailing
)
}

override fun equals(other: Any?): Boolean = other != null && (this::class == other::class)

override fun hashCode(): Int = this::class.hashCode()
}

fun <NavTarget : Any> BackStack<NavTarget>.singleTop(
navTarget: NavTarget,
mode: Operation.Mode = Operation.Mode.KEYFRAME,
animationSpec: AnimationSpec<Float>? = null
) {
operation(operation = SingleTop(navTarget, mode), animationSpec = animationSpec)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package com.bumble.appyx.components.stable.backstack.operation

import com.bumble.appyx.components.backstack.BackStackModel
import com.bumble.appyx.components.backstack.operation.SingleTop
import com.bumble.appyx.components.stable.backstack.TestTarget
import com.bumble.appyx.components.stable.backstack.TestTarget.Child1
import com.bumble.appyx.components.stable.backstack.TestTarget.Child2
import com.bumble.appyx.components.stable.backstack.TestTarget.Child3
import com.bumble.appyx.components.stable.backstack.TestTarget.Child4
import com.bumble.appyx.interactions.model.Element
import com.bumble.appyx.interactions.model.Elements
import com.bumble.appyx.interactions.model.asElement
import com.bumble.appyx.interactions.model.transition.StateTransition
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class SingleTopTest {

@Test
fun GIVEN_no_stashed_elements_THEN_it_is_applicable() {
val state = BackStackModel.State(active = Child1.asElement())

val singleTop = SingleTop(Child2)

assertTrue(singleTop.isApplicable(state))
}

@Test
fun GIVEN_some_stashed_elements_THEN_it_is_applicable() {
val state = BackStackModel.State(
active = Child1.asElement(),
stashed = elements(Child2)
)

val singleTop = SingleTop(Child3)

assertTrue(singleTop.isApplicable(state))
}

@Test
fun GIVEN_active_and_different_stashed_elements_THEN_makes_new_item_active_and_moves_old_active_to_stashed() {
val state = BackStackModel.State(
active = Child3.asElement(),
stashed = elements(Child1, Child2)
)

val actual = SingleTop(Child4).invoke(state)

actual.assertActive(Child4)

actual.assertStashed(Child1, Child2, Child3)
}

@Test
fun GIVEN_stashed_element_same_THEN_destroys_current_active_and_all_stashed_after_it_and_makes_matching_stashed_new_active() {
val state = BackStackModel.State(
active = Child4.asElement(),
stashed = elements(Child1, Child2, Child3)
)

val actual = SingleTop(Child2).invoke(state)

actual.assertActive(Child2)

actual.assertStashed(Child1)

actual.assertDestroyed(Child3, Child4)
}

@Test
fun GIVEN_active_element_same_THEN_does_nothing() {
val state = BackStackModel.State(
active = Child4.asElement(),
stashed = elements(Child1, Child2, Child3)
)

val actual = SingleTop(Child4).invoke(state)

actual.assertActive(Child4)

actual.assertStashed(Child1, Child2, Child3)

actual.assertDestroyed()
}

@Test
fun GIVEN_nothing_stashed_and_active_element_same_THEN_does_nothing() {
val state = BackStackModel.State(
active = Child1.asElement(),
stashed = emptyList(),
)

val actual = SingleTop(Child1).invoke(state)

actual.assertActive(Child1)

actual.assertStashed()

actual.assertDestroyed()
}

private fun StateTransition<BackStackModel.State<TestTarget>>.assertActive(expected: TestTarget) {
assertEquals(
actual = targetState.active.interactionTarget,
expected = expected,
message = "Active",
)
}

private fun StateTransition<BackStackModel.State<TestTarget>>.assertStashed(vararg expected: TestTarget) {
assertSame(
actual = targetState.stashed,
expected = expected.toList(),
message = "Stashed",
)
}

private fun StateTransition<BackStackModel.State<TestTarget>>.assertDestroyed(vararg expected: TestTarget) {
assertSame(
actual = targetState.destroyed,
expected = expected.toList(),
message = "Destroyed",
)
}

private fun elements(vararg elements: TestTarget): Elements<TestTarget> =
elements.toList().map { it.asElement() }

private fun assertSame(actual: Elements<TestTarget>, expected: List<TestTarget>, message: String) {
assertEquals(
actual = actual.map { it.interactionTarget }.toList(),
expected = expected,
message = message,
)
}
}