nowinandroid icon indicating copy to clipboard operation
nowinandroid copied to clipboard

Show snackbar in large screen

Open Jaehwa-Noh opened this issue 11 months ago • 14 comments

What I have done and why Add SafeDrawing bottom padding to show snackbar over bottom navigation bar

I'd set the windowInsetsPadding to NiaBackground, because SnackBar was drawn on the NiaBackground not NiaApp.

Fixes #1295

Foldable

For you Saved Interests
For you Saved Interests

Tablet

For you Saved Interests
For you Saved Interests

Jaehwa-Noh avatar Mar 18 '24 07:03 Jaehwa-Noh

Any chance to cover this with an UI behavior or screenshot regression test?

JoseAlcerreca avatar Mar 18 '24 10:03 JoseAlcerreca

@JoseAlcerreca Unfortunately, I can't. I try to make some tests, but assertIsDisplay is always true whether behind System UI or not. And screenshot test always throw exception

java.lang.RuntimeException: Unable to resolve activity for Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.google.samples.apps.nowinandroid.core.designsystem.test/androidx.activity.ComponentActivity }

Does it exception cause because of I am using Windows 11?

Jaehwa-Noh avatar Mar 18 '24 10:03 Jaehwa-Noh

How are you running the test?

JoseAlcerreca avatar Mar 18 '24 10:03 JoseAlcerreca

Oh It had discussed in issues. I forgotten. • Run with ./gradlew testDemoDebug The result as #1242

test


• Run test directly, you can see above mentioned exception.

By this reason, It is hard to make screenshot test on my own.

Jaehwa-Noh avatar Mar 18 '24 10:03 Jaehwa-Noh

I finally found the formular to check position, but it only work when the simulator was Foldable(Resizable), not Phone and Tablet. The problem is how to run tests only on Foldable device.

Test Code
/*
 * Copyright 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.samples.apps.nowinandroid.ui

import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
import androidx.compose.ui.test.hasAnyDescendant
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isSelectable
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.performClick
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.google.accompanist.testharness.TestHarness
import com.google.samples.apps.nowinandroid.MainActivity
import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.test.networkmonitor.AlwaysOfflineNetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule
import com.google.samples.apps.nowinandroid.extensions.stringResource
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import javax.inject.Inject

@HiltAndroidTest
class ConnectSnackBarTest {

    /**
     * Manages the components' state and is used to perform injection on your test
     */
    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)

    /**
     * Create a temporary folder used to create a Data Store file. This guarantees that
     * the file is removed in between each test, preventing a crash.
     */
    @BindValue
    @get:Rule(order = 1)
    val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()

    /**
     * Grant [android.Manifest.permission.POST_NOTIFICATIONS] permission.
     */
    @get:Rule(order = 2)
    val postNotificationsPermission = GrantPostNotificationsPermissionRule()

    /**
     * Use the primary activity to initialize the app normally.
     */
    @get:Rule(order = 3)
    val composeTestRule = createAndroidComposeRule<MainActivity>()

    @Inject
    lateinit var userNewsResourceRepository: CompositeUserNewsResourceRepository

    @Inject
    lateinit var timeZoneMonitor: TimeZoneMonitor

    private val networkMonitor: NetworkMonitor = AlwaysOfflineNetworkMonitor()

    private val forYou by composeTestRule.stringResource(com.google.samples.apps.nowinandroid.feature.foryou.R.string.feature_foryou_title)
    private val interests by composeTestRule.stringResource(com.google.samples.apps.nowinandroid.feature.interests.R.string.feature_interests_title)
    private val saved by composeTestRule.stringResource(com.google.samples.apps.nowinandroid.feature.bookmarks.R.string.feature_bookmarks_title)
    private val netConnected by composeTestRule.stringResource(R.string.not_connected)

    private var height = 0.dp
    private var bottomSafeDrawingHeight = 0.dp

    @Before
    fun setup() = hiltRule.inject()

    @Test
    fun mediumWidth_WhenNotConnectedAndForYou_ConnectSnackBarShowUp() {
        composeTestRule.activity.apply {
            setContent {
                TestHarness(size = DpSize(610.dp, 1000.dp)) {
                    BoxWithConstraints {
                        val density = LocalDensity.current
                        height = maxHeight
                        bottomSafeDrawingHeight =
                            WindowInsets.safeDrawing.getBottom(density = density).dp
                        NiaApp(
                            appState = fakeAppState(maxWidth, maxHeight),
                        )
                    }
                }
            }
        }

        composeTestRule.apply {
            findNavigationButton(forYou).apply {
                performClick()
                assertIsSelected()
            }

            findSnackbarWithMessage(message = netConnected)
                .assertIsDisplayed()
                .assertTopPositionInRootIsEqualTo(height - bottomSafeDrawingHeight)
        }
    }

    private fun findSnackbarWithMessage(message: String): SemanticsNodeInteraction =
        composeTestRule.onNode(
            matcher = hasTestTag("Snackbar") and
                hasAnyDescendant(matcher = hasText(message)),
        )

    private fun findNavigationButton(string: String): SemanticsNodeInteraction =
        composeTestRule.onNode(matcher = isSelectable() and hasText(string))

    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    @Composable
    private fun fakeAppState(maxWidth: Dp, maxHeight: Dp) = rememberNiaAppState(
        windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(maxWidth, maxHeight)),
        networkMonitor = networkMonitor,
        userNewsResourceRepository = userNewsResourceRepository,
        timeZoneMonitor = timeZoneMonitor,
    )
}
I would remain test parts to one who want to contribute.

Jaehwa-Noh avatar Mar 18 '24 11:03 Jaehwa-Noh

~~I added test case. But I don't say that this is best practices, but It is the only way to test, now.~~

Edit: There's no way to test it. The git action device is Phone size, and there are no Tablet or Foldable to test.

Jaehwa-Noh avatar Mar 19 '24 06:03 Jaehwa-Noh

I breakthrough the testing by check the height of spacer. If the spacer height is same as SafeDraw bottom height, It guarantee the snackbar will have shown above the System UI.

Jaehwa-Noh avatar Mar 22 '24 11:03 Jaehwa-Noh

The test failed from testharness.

java.lang.ClassCastException: com.google.accompanist.testharness.ContextThemeWrapper cannot be cast to android.app.Activity at androidx.window.layout.WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(WindowMetricsCalculatorCompat.kt:68

The testharness deprecated so that I wait to bump Compose to use DeviceConfigurationOverride

Note: DeviceConfigurationOverride is a new API first available in Compose 1.7.0-alpha03

And I start discussion for screenshot test #1347, for this issue #1295.

Jaehwa-Noh avatar Mar 27 '24 11:03 Jaehwa-Noh

Thank you for your review. Could you tell me more details?

For testing, how about you add it to https://github.com/android/nowinandroid/pull/1343?

  1. Does it mean I'd merge your branch into this branch?

You are checking for dimensions, so screenshots are a much easier way to do it.

  1. This indicates this code?
  Spacer(
      modifier = Modifier
          .height(bottomNavigationHeight)
          .semantics {
              testTag = "Bottom padding for snackbar"
          },
  )


I think you can do this by overriding WindowInsetsHolder? It has a setUseTestInsets function, but I haven't looked into this.

  1. This suggestion for what situation? testharness fail problems? or screenshot tests? And could you give me a documentation or link about WindowInsetsHolder, I can't find it on android developer documentation.

Best regards.

Jaehwa-Noh avatar Mar 28 '24 07:03 Jaehwa-Noh

Does Anyone have ideas giving the fake insets to WindowInsets.safeDrawing to test screenshot test? My code does avoid System UI by calculate WindowInsets.safeDrawing.

val bottomNavigationHeight = density.run {
        WindowInsets.safeDrawing
            .getBottom(density = density)
            .toDp()
    }

Kindly leave your ideas in #1347

If you think my instrumented test ConnectSnackBarTest.kt is sufficient to test that avoid System UI logic, kindly merge this pull request.

Jaehwa-Noh avatar Apr 16 '24 08:04 Jaehwa-Noh

I'd added screenshots. Done.

Jaehwa-Noh avatar Apr 17 '24 05:04 Jaehwa-Noh

With the merging of #1498 , we should be able to directly test the fix here for the snackbar across device configurations. Would you be able to rebase on top of latest to check the result?

alexvanyo avatar Jul 02 '24 16:07 alexvanyo

@alexvanyo Thank you for introducing. I'll try it or re-open this PR.

Jaehwa-Noh avatar Jul 03 '24 00:07 Jaehwa-Noh

@alexvanyo @JoseAlcerreca If you all have some spare time, kindly check this PR. I changed some screenshot test code from #1498, because of the SnackBar was drawn on NiaBackground not on NiaApp. It seemed to be done after Navigation library updated.

Thank you all.

Jaehwa-Noh avatar Jul 03 '24 07:07 Jaehwa-Noh