From cc29669cfb010969dead05fd2579c1641fd72848 Mon Sep 17 00:00:00 2001 From: Ray Ryan Date: Thu, 9 Mar 2023 11:16:52 -0800 Subject: [PATCH 1/4] cp samples/{hellow-workflow,nested-overlays} --- samples/nested-overlays/build.gradle.kts | 25 +++++++++++ .../nestedoverlays/NestedOverlaysAppTest.kt | 40 +++++++++++++++++ .../src/main/AndroidManifest.xml | 23 ++++++++++ .../sample/nestedoverlays/HelloRendering.kt | 19 ++++++++ .../nestedoverlays/NestedOverlaysActivity.kt | 43 +++++++++++++++++++ .../nestedoverlays/NestedOverlaysWorkflow.kt | 42 ++++++++++++++++++ .../main/res/layout/hello_goodbye_layout.xml | 14 ++++++ .../src/main/res/values/strings.xml | 3 ++ .../src/main/res/values/styles.xml | 8 ++++ settings.gradle.kts | 1 + 10 files changed, 218 insertions(+) create mode 100644 samples/nested-overlays/build.gradle.kts create mode 100644 samples/nested-overlays/src/androidTest/java/com/squareup/sample/nestedoverlays/NestedOverlaysAppTest.kt create mode 100644 samples/nested-overlays/src/main/AndroidManifest.xml create mode 100644 samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/HelloRendering.kt create mode 100644 samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysActivity.kt create mode 100644 samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysWorkflow.kt create mode 100644 samples/nested-overlays/src/main/res/layout/hello_goodbye_layout.xml create mode 100644 samples/nested-overlays/src/main/res/values/strings.xml create mode 100644 samples/nested-overlays/src/main/res/values/styles.xml diff --git a/samples/nested-overlays/build.gradle.kts b/samples/nested-overlays/build.gradle.kts new file mode 100644 index 0000000000..7d31f7a461 --- /dev/null +++ b/samples/nested-overlays/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("com.android.application") + `kotlin-android` + `android-sample-app` + `android-ui-tests` +} + +android { + defaultConfig { + applicationId = "com.squareup.sample.nestedoverlays" + } + namespace = "com.squareup.sample.nestedoverlays" +} + +dependencies { + debugImplementation(libs.squareup.leakcanary.android) + + implementation(libs.androidx.activity.ktx) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.viewmodel.savedstate) + implementation(libs.androidx.viewbinding) + + implementation(project(":workflow-ui:core-android")) + implementation(project(":workflow-ui:core-common")) +} diff --git a/samples/nested-overlays/src/androidTest/java/com/squareup/sample/nestedoverlays/NestedOverlaysAppTest.kt b/samples/nested-overlays/src/androidTest/java/com/squareup/sample/nestedoverlays/NestedOverlaysAppTest.kt new file mode 100644 index 0000000000..5d20722b29 --- /dev/null +++ b/samples/nested-overlays/src/androidTest/java/com/squareup/sample/nestedoverlays/NestedOverlaysAppTest.kt @@ -0,0 +1,40 @@ +package com.squareup.sample.nestedoverlays + +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule +import com.squareup.workflow1.ui.internal.test.inAnyView +import leakcanary.DetectLeaksAfterTestSuccess +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@OptIn(WorkflowUiExperimentalApi::class) +class NestedOverlaysAppTest { + + private val scenarioRule = ActivityScenarioRule(NestedOverlaysActivity::class.java) + + @get:Rule val rules = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) + .around(scenarioRule) + .around(IdlingDispatcherRule) + + @Test fun togglesHelloAndGoodbye() { + inAnyView(withText("Hello")) + .check(matches(isDisplayed())) + .perform(click()) + + inAnyView(withText("Goodbye")) + .check(matches(isDisplayed())) + .perform(click()) + + inAnyView(withText("Hello")) + .check(matches(isDisplayed())) + } +} diff --git a/samples/nested-overlays/src/main/AndroidManifest.xml b/samples/nested-overlays/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..594e1fde1d --- /dev/null +++ b/samples/nested-overlays/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/HelloRendering.kt b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/HelloRendering.kt new file mode 100644 index 0000000000..3087d9deda --- /dev/null +++ b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/HelloRendering.kt @@ -0,0 +1,19 @@ +package com.squareup.sample.nestedoverlays + +import com.squareup.sample.nestedoverlays.databinding.HelloGoodbyeLayoutBinding +import com.squareup.workflow1.ui.AndroidScreen +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +@OptIn(WorkflowUiExperimentalApi::class) +data class HelloRendering( + val message: String, + val onClick: () -> Unit +) : AndroidScreen { + override val viewFactory: ScreenViewFactory = + fromViewBinding(HelloGoodbyeLayoutBinding::inflate) { r, _ -> + helloMessage.text = r.message + helloMessage.setOnClickListener { r.onClick() } + } +} diff --git a/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysActivity.kt b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysActivity.kt new file mode 100644 index 0000000000..44b5b2b2d7 --- /dev/null +++ b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysActivity.kt @@ -0,0 +1,43 @@ +@file:OptIn(WorkflowExperimentalRuntime::class) + +package com.squareup.sample.nestedoverlays + +import android.os.Bundle +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.squareup.workflow1.WorkflowExperimentalRuntime +import com.squareup.workflow1.config.AndroidRuntimeConfigTools +import com.squareup.workflow1.ui.WorkflowLayout +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.renderWorkflowIn +import kotlinx.coroutines.flow.StateFlow + +@OptIn(WorkflowUiExperimentalApi::class) +class NestedOverlaysActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // This ViewModel will survive configuration changes. It's instantiated + // by the first call to viewModels(), and that original instance is returned by + // succeeding calls. + val model: HelloViewModel by viewModels() + setContentView( + WorkflowLayout(this).apply { take(lifecycle, model.renderings) } + ) + } +} + +class HelloViewModel(savedState: SavedStateHandle) : ViewModel() { + @OptIn(WorkflowUiExperimentalApi::class) + val renderings: StateFlow by lazy { + renderWorkflowIn( + workflow = NestedOverlaysWorkflow, + scope = viewModelScope, + savedStateHandle = savedState, + runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig() + ) + } +} diff --git a/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysWorkflow.kt b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysWorkflow.kt new file mode 100644 index 0000000000..9503cf7051 --- /dev/null +++ b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysWorkflow.kt @@ -0,0 +1,42 @@ +package com.squareup.sample.nestedoverlays + +import com.squareup.sample.nestedoverlays.NestedOverlaysWorkflow.State +import com.squareup.sample.nestedoverlays.NestedOverlaysWorkflow.State.Goodbye +import com.squareup.sample.nestedoverlays.NestedOverlaysWorkflow.State.Hello +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.StatefulWorkflow +import com.squareup.workflow1.action +import com.squareup.workflow1.parse + +object NestedOverlaysWorkflow : StatefulWorkflow() { + enum class State { + Hello, + Goodbye + } + + override fun initialState( + props: Unit, + snapshot: Snapshot? + ): State = snapshot?.bytes?.parse { source -> if (source.readInt() == 1) Hello else Goodbye } + ?: Hello + + override fun render( + renderProps: Unit, + renderState: State, + context: RenderContext + ): HelloRendering { + return HelloRendering( + message = renderState.name, + onClick = { context.actionSink.send(helloAction) } + ) + } + + override fun snapshotState(state: State): Snapshot = Snapshot.of(if (state == Hello) 1 else 0) + + private val helloAction = action { + state = when (state) { + Hello -> Goodbye + Goodbye -> Hello + } + } +} diff --git a/samples/nested-overlays/src/main/res/layout/hello_goodbye_layout.xml b/samples/nested-overlays/src/main/res/layout/hello_goodbye_layout.xml new file mode 100644 index 0000000000..dcd6f7c0b6 --- /dev/null +++ b/samples/nested-overlays/src/main/res/layout/hello_goodbye_layout.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/samples/nested-overlays/src/main/res/values/strings.xml b/samples/nested-overlays/src/main/res/values/strings.xml new file mode 100644 index 0000000000..eaa5276c97 --- /dev/null +++ b/samples/nested-overlays/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Nested Overlays + diff --git a/samples/nested-overlays/src/main/res/values/styles.xml b/samples/nested-overlays/src/main/res/values/styles.xml new file mode 100644 index 0000000000..e2331afcc2 --- /dev/null +++ b/samples/nested-overlays/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 662672fedb..c2a8ddcac1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -55,6 +55,7 @@ include( ":samples:hello-terminal:todo-terminal-app", ":samples:hello-workflow", ":samples:hello-workflow-fragment", + ":samples:nested-overlays", ":samples:stub-visibility", ":samples:tictactoe:app", ":samples:tictactoe:common", From fbfda508ef331ee039f7970f03b633c18d049162 Mon Sep 17 00:00:00 2001 From: Ray Ryan Date: Thu, 9 Mar 2023 14:44:59 -0800 Subject: [PATCH 2/4] 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. --- samples/nested-overlays/lint-baseline.xml | 4 + .../nestedoverlays/NestedOverlaysAppTest.kt | 85 +++++++++++--- .../sample/nestedoverlays/ButtonBar.kt | 52 +++++++++ .../sample/nestedoverlays/HelloRendering.kt | 19 ---- .../nestedoverlays/NestedOverlaysActivity.kt | 7 +- .../nestedoverlays/NestedOverlaysWorkflow.kt | 107 ++++++++++++++---- .../nestedoverlays/TopAndBottomBarsScreen.kt | 31 +++++ .../main/res/layout/hello_goodbye_layout.xml | 14 --- .../main/res/layout/top_and_bottom_bars.xml | 28 +++++ .../src/main/res/values/strings.xml | 10 ++ .../container/ScreenOverlayDialogFactory.kt | 3 +- .../ui/container/BodyAndOverlaysScreen.kt | 15 ++- 12 files changed, 300 insertions(+), 75 deletions(-) create mode 100644 samples/nested-overlays/lint-baseline.xml create mode 100644 samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/ButtonBar.kt delete mode 100644 samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/HelloRendering.kt create mode 100644 samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/TopAndBottomBarsScreen.kt delete mode 100644 samples/nested-overlays/src/main/res/layout/hello_goodbye_layout.xml create mode 100644 samples/nested-overlays/src/main/res/layout/top_and_bottom_bars.xml diff --git a/samples/nested-overlays/lint-baseline.xml b/samples/nested-overlays/lint-baseline.xml new file mode 100644 index 0000000000..4aec7fcd50 --- /dev/null +++ b/samples/nested-overlays/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/samples/nested-overlays/src/androidTest/java/com/squareup/sample/nestedoverlays/NestedOverlaysAppTest.kt b/samples/nested-overlays/src/androidTest/java/com/squareup/sample/nestedoverlays/NestedOverlaysAppTest.kt index 5d20722b29..bebcd2ac73 100644 --- a/samples/nested-overlays/src/androidTest/java/com/squareup/sample/nestedoverlays/NestedOverlaysAppTest.kt +++ b/samples/nested-overlays/src/androidTest/java/com/squareup/sample/nestedoverlays/NestedOverlaysAppTest.kt @@ -1,40 +1,99 @@ package com.squareup.sample.nestedoverlays +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withParent +import androidx.test.espresso.matcher.ViewMatchers.withParentIndex import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule -import com.squareup.workflow1.ui.internal.test.inAnyView import leakcanary.DetectLeaksAfterTestSuccess +import org.hamcrest.core.AllOf.allOf +import org.hamcrest.core.IsNot.not import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -@OptIn(WorkflowUiExperimentalApi::class) class NestedOverlaysAppTest { private val scenarioRule = ActivityScenarioRule(NestedOverlaysActivity::class.java) - @get:Rule val rules = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) + @get:Rule val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) .around(scenarioRule) .around(IdlingDispatcherRule) - @Test fun togglesHelloAndGoodbye() { - inAnyView(withText("Hello")) - .check(matches(isDisplayed())) - .perform(click()) + @Test fun basics() { + onTopCoverBody().assertDisplayed() + onTopCoverEverything().assertDisplayed() + onBottomCoverBody().assertDisplayed() + onBottomCoverEverything().assertDisplayed() - inAnyView(withText("Goodbye")) - .check(matches(isDisplayed())) - .perform(click()) + onTopCoverBody().perform(click()) + onView(withText("Close")).perform(click()) + onTopCoverEverything().perform(click()) + onView(withText("Close")).perform(click()) - inAnyView(withText("Hello")) - .check(matches(isDisplayed())) + onView(withText("Hide Top Bar")).perform(click()) + onTopCoverBody().assertNotDisplayed() + onTopCoverEverything().assertNotDisplayed() + onBottomCoverBody().assertDisplayed() + onBottomCoverEverything().assertDisplayed() + + onView(withText("Hide Bottom Bar")).perform(click()) + onTopCoverBody().assertNotDisplayed() + onTopCoverEverything().assertNotDisplayed() + onBottomCoverBody().assertNotDisplayed() + onBottomCoverEverything().assertNotDisplayed() + } + + // https://github.com/square/workflow-kotlin/issues/966 + @Test fun canInsertDialog() { + onTopCoverEverything().perform(click()) + onView(withText("Hide Top Bar")).check(doesNotExist()) + onView(withText("Cover Body")).perform(click()) + + // This line fails due to https://github.com/square/workflow-kotlin/issues/966 + // onView(withText("Hide Top Bar")).check(doesNotExist()) + + // Should continue to close the top sheet and assert that the inner sheet is visible. + } + + // So far can't express this in Espresso. Considering move to Maestro + // @Test fun canClickPastInnerWindow() { + // onView(allOf(withText("Cover Everything"), withParent(withParentIndex(0)))) + // .perform(click()) + // + // scenario.onActivity { activity -> + // onView(allOf(withText("Cover Everything"), withParent(withParentIndex(0)))) + // .inRoot(withDecorView(not(`is`(activity.window.decorView)))) + // .perform(click()) + // } + // } + + private fun ViewInteraction.assertNotDisplayed() { + check(matches(not(isDisplayed()))) } + + private fun ViewInteraction.assertDisplayed() { + check(matches(isDisplayed())) + } + + private fun onBottomCoverEverything() = + onView(allOf(withText("Cover Everything"), withParent(withParentIndex(2)))) + + private fun onBottomCoverBody() = + onView(allOf(withText("Cover Body"), withParent(withParentIndex(2)))) + + private fun onTopCoverBody() = + onView(allOf(withText("Cover Body"), withParent(withParentIndex(0)))) + + private fun onTopCoverEverything() = + onView(allOf(withText("Cover Everything"), withParent(withParentIndex(0)))) } diff --git a/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/ButtonBar.kt b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/ButtonBar.kt new file mode 100644 index 0000000000..ce85e72f31 --- /dev/null +++ b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/ButtonBar.kt @@ -0,0 +1,52 @@ +package com.squareup.sample.nestedoverlays + +import android.graphics.drawable.ColorDrawable +import android.view.Gravity +import android.widget.LinearLayout +import androidx.annotation.ColorRes +import androidx.annotation.StringRes +import androidx.core.view.get +import com.squareup.workflow1.ui.AndroidScreen +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewHolder +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import android.widget.Button as ButtonView + +data class Button( + @StringRes val name: Int, + val onClick: () -> Unit +) + +@OptIn(WorkflowUiExperimentalApi::class) +class ButtonBar( + vararg buttons: Button?, + @ColorRes val color: Int = -1, +) : AndroidScreen { + val buttons: List