Skip to content

Commit ed52843

Browse files
committed
Nested overlays sample uses overlays.
And can demonstrate the bug where dialogs are shown out of order: - Click _Cover Everything_ - Click _Cover Body_ The red inner dialog is shown over the outer green dialog, but the green one should always be on top. (#966) Also introduces `name` parameter for `BodyAndOverlaysScreen`, because they're a nightmare to nest without it. See the kdoc for details.
1 parent cc29669 commit ed52843

File tree

11 files changed

+296
-75
lines changed

11 files changed

+296
-75
lines changed
Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,99 @@
11
package com.squareup.sample.nestedoverlays
22

3+
import androidx.test.espresso.Espresso.onView
4+
import androidx.test.espresso.ViewInteraction
35
import androidx.test.espresso.action.ViewActions.click
6+
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
47
import androidx.test.espresso.assertion.ViewAssertions.matches
58
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
9+
import androidx.test.espresso.matcher.ViewMatchers.withParent
10+
import androidx.test.espresso.matcher.ViewMatchers.withParentIndex
611
import androidx.test.espresso.matcher.ViewMatchers.withText
712
import androidx.test.ext.junit.rules.ActivityScenarioRule
813
import androidx.test.ext.junit.runners.AndroidJUnit4
9-
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
1014
import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule
11-
import com.squareup.workflow1.ui.internal.test.inAnyView
1215
import leakcanary.DetectLeaksAfterTestSuccess
16+
import org.hamcrest.core.AllOf.allOf
17+
import org.hamcrest.core.IsNot.not
1318
import org.junit.Rule
1419
import org.junit.Test
1520
import org.junit.rules.RuleChain
1621
import org.junit.runner.RunWith
1722

1823
@RunWith(AndroidJUnit4::class)
19-
@OptIn(WorkflowUiExperimentalApi::class)
2024
class NestedOverlaysAppTest {
2125

2226
private val scenarioRule = ActivityScenarioRule(NestedOverlaysActivity::class.java)
2327

24-
@get:Rule val rules = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
28+
@get:Rule val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
2529
.around(scenarioRule)
2630
.around(IdlingDispatcherRule)
2731

28-
@Test fun togglesHelloAndGoodbye() {
29-
inAnyView(withText("Hello"))
30-
.check(matches(isDisplayed()))
31-
.perform(click())
32+
@Test fun basics() {
33+
onTopCoverBody().assertDisplayed()
34+
onTopCoverEverything().assertDisplayed()
35+
onBottomCoverBody().assertDisplayed()
36+
onBottomCoverEverything().assertDisplayed()
3237

33-
inAnyView(withText("Goodbye"))
34-
.check(matches(isDisplayed()))
35-
.perform(click())
38+
onTopCoverBody().perform(click())
39+
onView(withText("Close")).perform(click())
40+
onTopCoverEverything().perform(click())
41+
onView(withText("Close")).perform(click())
3642

37-
inAnyView(withText("Hello"))
38-
.check(matches(isDisplayed()))
43+
onView(withText("Hide Top Bar")).perform(click())
44+
onTopCoverBody().assertNotDisplayed()
45+
onTopCoverEverything().assertNotDisplayed()
46+
onBottomCoverBody().assertDisplayed()
47+
onBottomCoverEverything().assertDisplayed()
48+
49+
onView(withText("Hide Bottom Bar")).perform(click())
50+
onTopCoverBody().assertNotDisplayed()
51+
onTopCoverEverything().assertNotDisplayed()
52+
onBottomCoverBody().assertNotDisplayed()
53+
onBottomCoverEverything().assertNotDisplayed()
54+
}
55+
56+
// https://github.com/square/workflow-kotlin/issues/966
57+
@Test fun canInsertDialog() {
58+
onTopCoverEverything().perform(click())
59+
onView(withText("Hide Top Bar")).check(doesNotExist())
60+
onView(withText("Cover Body")).perform(click())
61+
62+
// This line fails due to https://github.com/square/workflow-kotlin/issues/966
63+
// onView(withText("Hide Top Bar")).check(doesNotExist())
64+
65+
// Should continue to close the top sheet and assert that the inner sheet is visible.
66+
}
67+
68+
// So far can't express this in Espresso. Considering move to Maestro
69+
// @Test fun canClickPastInnerWindow() {
70+
// onView(allOf(withText("Cover Everything"), withParent(withParentIndex(0))))
71+
// .perform(click())
72+
//
73+
// scenario.onActivity { activity ->
74+
// onView(allOf(withText("Cover Everything"), withParent(withParentIndex(0))))
75+
// .inRoot(withDecorView(not(`is`(activity.window.decorView))))
76+
// .perform(click())
77+
// }
78+
// }
79+
80+
private fun ViewInteraction.assertNotDisplayed() {
81+
check(matches(not(isDisplayed())))
3982
}
83+
84+
private fun ViewInteraction.assertDisplayed() {
85+
check(matches(isDisplayed()))
86+
}
87+
88+
private fun onBottomCoverEverything() =
89+
onView(allOf(withText("Cover Everything"), withParent(withParentIndex(2))))
90+
91+
private fun onBottomCoverBody() =
92+
onView(allOf(withText("Cover Body"), withParent(withParentIndex(2))))
93+
94+
private fun onTopCoverBody() =
95+
onView(allOf(withText("Cover Body"), withParent(withParentIndex(0))))
96+
97+
private fun onTopCoverEverything() =
98+
onView(allOf(withText("Cover Everything"), withParent(withParentIndex(0))))
4099
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.squareup.sample.nestedoverlays
2+
3+
import android.graphics.drawable.ColorDrawable
4+
import android.view.Gravity
5+
import android.widget.LinearLayout
6+
import androidx.annotation.ColorRes
7+
import androidx.annotation.StringRes
8+
import androidx.core.view.get
9+
import com.squareup.workflow1.ui.AndroidScreen
10+
import com.squareup.workflow1.ui.ScreenViewFactory
11+
import com.squareup.workflow1.ui.ScreenViewHolder
12+
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
13+
import android.widget.Button as ButtonView
14+
15+
data class Button(
16+
@StringRes val name: Int,
17+
val onClick: () -> Unit
18+
)
19+
20+
@OptIn(WorkflowUiExperimentalApi::class)
21+
class ButtonBar(
22+
vararg buttons: Button?,
23+
@ColorRes val color: Int = -1,
24+
) : AndroidScreen<ButtonBar> {
25+
val buttons: List<Button> = buttons.filterNotNull().toList()
26+
27+
override val viewFactory =
28+
ScreenViewFactory.fromCode<ButtonBar> { _, initialEnvironment, context, _ ->
29+
LinearLayout(context).let { view ->
30+
if (color > -1) view.background = ColorDrawable(view.resources.getColor(color))
31+
32+
view.gravity = Gravity.CENTER
33+
34+
ScreenViewHolder(initialEnvironment, view) { bar, _ ->
35+
val existing = view.childCount
36+
37+
bar.buttons.forEachIndexed { index, button ->
38+
val buttonView = if (index < existing) {
39+
view[index] as ButtonView
40+
} else {
41+
ButtonView(context).also { view.addView(it) }
42+
}
43+
with(buttonView) {
44+
text = view.resources.getText(button.name)
45+
setOnClickListener { button.onClick() }
46+
}
47+
}
48+
for (i in bar.buttons.size until view.childCount) view.removeViewAt(i)
49+
}
50+
}
51+
}
52+
}

samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/HelloRendering.kt

Lines changed: 0 additions & 19 deletions
This file was deleted.

samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysActivity.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel
1010
import androidx.lifecycle.viewModelScope
1111
import com.squareup.workflow1.WorkflowExperimentalRuntime
1212
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
13+
import com.squareup.workflow1.ui.Screen
1314
import com.squareup.workflow1.ui.WorkflowLayout
1415
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
1516
import com.squareup.workflow1.ui.renderWorkflowIn
@@ -23,16 +24,16 @@ class NestedOverlaysActivity : AppCompatActivity() {
2324
// This ViewModel will survive configuration changes. It's instantiated
2425
// by the first call to viewModels(), and that original instance is returned by
2526
// succeeding calls.
26-
val model: HelloViewModel by viewModels()
27+
val model: NestedOverlaysViewModel by viewModels()
2728
setContentView(
2829
WorkflowLayout(this).apply { take(lifecycle, model.renderings) }
2930
)
3031
}
3132
}
3233

33-
class HelloViewModel(savedState: SavedStateHandle) : ViewModel() {
34+
class NestedOverlaysViewModel(savedState: SavedStateHandle) : ViewModel() {
3435
@OptIn(WorkflowUiExperimentalApi::class)
35-
val renderings: StateFlow<HelloRendering> by lazy {
36+
val renderings: StateFlow<Screen> by lazy {
3637
renderWorkflowIn(
3738
workflow = NestedOverlaysWorkflow,
3839
scope = viewModelScope,
Lines changed: 85 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,105 @@
1+
@file:OptIn(WorkflowUiExperimentalApi::class)
2+
13
package com.squareup.sample.nestedoverlays
24

35
import com.squareup.sample.nestedoverlays.NestedOverlaysWorkflow.State
4-
import com.squareup.sample.nestedoverlays.NestedOverlaysWorkflow.State.Goodbye
5-
import com.squareup.sample.nestedoverlays.NestedOverlaysWorkflow.State.Hello
66
import com.squareup.workflow1.Snapshot
77
import com.squareup.workflow1.StatefulWorkflow
8-
import com.squareup.workflow1.action
9-
import com.squareup.workflow1.parse
8+
import com.squareup.workflow1.ui.Screen
9+
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
10+
import com.squareup.workflow1.ui.container.BodyAndOverlaysScreen
11+
import com.squareup.workflow1.ui.container.FullScreenOverlay
12+
import com.squareup.workflow1.ui.container.Overlay
1013

11-
object NestedOverlaysWorkflow : StatefulWorkflow<Unit, State, Nothing, HelloRendering>() {
12-
enum class State {
13-
Hello,
14-
Goodbye
15-
}
14+
typealias NestedOverlaysRendering = BodyAndOverlaysScreen<
15+
TopAndBottomBarsScreen<BodyAndOverlaysScreen<Screen, Overlay>>,
16+
Overlay
17+
>
18+
19+
object NestedOverlaysWorkflow : StatefulWorkflow<Unit, State, Nothing, NestedOverlaysRendering>() {
20+
data class State(
21+
val showTopBar: Boolean = true,
22+
val showBottomBar: Boolean = true,
23+
val showInnerSheet: Boolean = false,
24+
val showOuterSheet: Boolean = false
25+
)
1626

1727
override fun initialState(
1828
props: Unit,
1929
snapshot: Snapshot?
20-
): State = snapshot?.bytes?.parse { source -> if (source.readInt() == 1) Hello else Goodbye }
21-
?: Hello
30+
) = State()
2231

2332
override fun render(
2433
renderProps: Unit,
2534
renderState: State,
2635
context: RenderContext
27-
): HelloRendering {
28-
return HelloRendering(
29-
message = renderState.name,
30-
onClick = { context.actionSink.send(helloAction) }
36+
): NestedOverlaysRendering {
37+
val toggleTopBarButton = Button(
38+
name = if (renderState.showTopBar) R.string.HIDE_TOP else R.string.SHOW_TOP,
39+
onClick = context.eventHandler { state = state.copy(showTopBar = !state.showTopBar) }
3140
)
32-
}
3341

34-
override fun snapshotState(state: State): Snapshot = Snapshot.of(if (state == Hello) 1 else 0)
42+
val toggleBottomBarButton = Button(
43+
name = if (renderState.showBottomBar) R.string.HIDE_BOTTOM else R.string.SHOW_BOTTOM,
44+
onClick = context.eventHandler { state = state.copy(showBottomBar = !state.showBottomBar) }
45+
)
46+
47+
val outerSheet = if (!renderState.showOuterSheet) null else FullScreenOverlay(
48+
ButtonBar(
49+
Button(
50+
name = R.string.CLOSE,
51+
onClick = context.eventHandler { state = state.copy(showOuterSheet = false) }
52+
),
53+
context.toggleInnerSheetButton(renderState),
54+
color = android.R.color.holo_green_light
55+
)
56+
)
3557

36-
private val helloAction = action {
37-
state = when (state) {
38-
Hello -> Goodbye
39-
Goodbye -> Hello
40-
}
58+
val innerSheet = if (!renderState.showInnerSheet) null else FullScreenOverlay(
59+
ButtonBar(
60+
Button(
61+
name = R.string.CLOSE,
62+
onClick = context.eventHandler { state = state.copy(showInnerSheet = false) }
63+
),
64+
toggleTopBarButton,
65+
toggleBottomBarButton,
66+
color = android.R.color.holo_red_light
67+
)
68+
)
69+
val bodyBarButtons = ButtonBar(toggleTopBarButton, toggleBottomBarButton)
70+
71+
return BodyAndOverlaysScreen(
72+
name = "outer",
73+
overlays = listOfNotNull(outerSheet),
74+
body = TopAndBottomBarsScreen(
75+
topBar = if (!renderState.showTopBar) null else context.topBottomBar(renderState),
76+
content = BodyAndOverlaysScreen(
77+
name = "inner",
78+
body = bodyBarButtons,
79+
overlays = listOfNotNull(innerSheet)
80+
),
81+
bottomBar = if (!renderState.showBottomBar) null else context.topBottomBar(renderState)
82+
)
83+
)
4184
}
85+
86+
override fun snapshotState(state: State) = null
87+
88+
private fun RenderContext.topBottomBar(
89+
renderState: State
90+
) = ButtonBar(
91+
toggleInnerSheetButton(renderState),
92+
Button(
93+
name = R.string.COVER_ALL,
94+
onClick = eventHandler { state = state.copy(showOuterSheet = true) }
95+
)
96+
)
97+
98+
private fun RenderContext.toggleInnerSheetButton(renderState: State) =
99+
Button(
100+
name = if (renderState.showInnerSheet) R.string.REVEAL_BODY else R.string.COVER_BODY,
101+
onClick = eventHandler {
102+
state = state.copy(showInnerSheet = !state.showInnerSheet)
103+
}
104+
)
42105
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.squareup.sample.nestedoverlays
2+
3+
import android.view.View.GONE
4+
import android.view.View.VISIBLE
5+
import com.squareup.sample.nestedoverlays.databinding.TopAndBottomBarsBinding
6+
import com.squareup.workflow1.ui.AndroidScreen
7+
import com.squareup.workflow1.ui.Screen
8+
import com.squareup.workflow1.ui.ScreenViewFactory
9+
import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding
10+
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
11+
import com.squareup.workflow1.ui.Wrapper
12+
13+
@OptIn(WorkflowUiExperimentalApi::class)
14+
data class TopAndBottomBarsScreen<T : Screen>(
15+
override val content: T,
16+
val topBar: ButtonBar? = null,
17+
val bottomBar: ButtonBar? = null
18+
) : AndroidScreen<TopAndBottomBarsScreen<T>>, Wrapper<Screen, T> {
19+
override fun <ContentU : Screen> map(transform: (T) -> ContentU) =
20+
TopAndBottomBarsScreen(transform(content), topBar, bottomBar)
21+
22+
override val viewFactory: ScreenViewFactory<TopAndBottomBarsScreen<T>> =
23+
fromViewBinding(TopAndBottomBarsBinding::inflate) { screen, environment ->
24+
bodyStub.show(screen.content, environment)
25+
26+
screen.topBar?.let { topBarStub.show(it, environment) }
27+
screen.bottomBar?.let { bottomBarStub.show(it, environment) }
28+
topBarStub.actual.visibility = if (screen.topBar != null) VISIBLE else GONE
29+
bottomBarStub.actual.visibility = if (screen.bottomBar != null) VISIBLE else GONE
30+
}
31+
}

samples/nested-overlays/src/main/res/layout/hello_goodbye_layout.xml

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)