Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 64e175eb authored by Sandeep Suman's avatar Sandeep Suman Committed by Android (Google) Code Review
Browse files

Merge "Added test for volume sliders and media output component" into main

parents 655f61a7 a1ad4c56
Loading
Loading
Loading
Loading
+7 −1
Original line number Diff line number Diff line
@@ -32,7 +32,7 @@ import androidx.compose.ui.semantics.testTagsAsResourceId
 */
@Stable
fun Modifier.sysuiResTag(resId: String): Modifier {
    return this.testTag("com.android.systemui:id/$resId")
    return this.testTag(resIdToTestTag(resId))
}

/** Mark this node as a container that contains one or more [sysuiResTag] descendants. */
@@ -41,4 +41,10 @@ fun Modifier.sysUiResTagContainer(): Modifier {
    return this.then(TestTagAsResourceIdModifier)
}

/**
 * Converts a simple resource ID name string into a fully qualified resource name string, formatted
 * for use as a test tag within the Android SystemUI package.
 */
fun resIdToTestTag(resId: String): String = "com.android.systemui:id/$resId"

private val TestTagAsResourceIdModifier = Modifier.semantics { testTagsAsResourceId = true }
+59 −19
Original line number Diff line number Diff line
@@ -45,7 +45,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -58,6 +60,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.Expandable
import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.common.ui.compose.toColor
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.res.R
import com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel.ConnectedDeviceViewModel
import com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel.DeviceIconViewModel
@@ -65,14 +68,15 @@ import com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel.Medi
import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent
import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope
import com.google.common.annotations.VisibleForTesting
import java.util.Objects
import javax.inject.Inject
import platform.test.motion.compose.values.MotionTestValueKey
import platform.test.motion.compose.values.motionTestValues

@VolumePanelScope
class MediaOutputComponent
@Inject
constructor(
    private val viewModel: MediaOutputViewModel,
) : ComposeVolumePanelUiComponent {
class MediaOutputComponent @Inject constructor(private val viewModel: MediaOutputViewModel) :
    ComposeVolumePanelUiComponent {

    @Composable
    override fun VolumePanelComposeScope.Content(modifier: Modifier) {
@@ -142,9 +146,19 @@ constructor(
    @Composable
    private fun ConnectedDeviceIcon(deviceIconViewModel: DeviceIconViewModel) {
        val transition = updateTransition(deviceIconViewModel, label = "MediaOutputIconTransition")
        val isTransitionIdle by
            remember(transition) {
                derivedStateOf {
                    transition.currentState == transition.targetState && !transition.isRunning
                }
            }
        Box(
            modifier = Modifier.padding(16.dp).fillMaxHeight().aspectRatio(1f),
            contentAlignment = Alignment.Center
            modifier =
                Modifier.padding(16.dp).fillMaxHeight().aspectRatio(1f).motionTestValues {
                    isTransitionIdle exportAs
                        MediaOutputComponentMotionTestKeys.isIconTransitionIdle
                },
            contentAlignment = Alignment.Center,
        ) {
            transition.AnimatedContent(
                contentKey = { it.backgroundColor },
@@ -157,12 +171,10 @@ constructor(
                            fadeOut(animationSpec = snap())
                    } else {
                        fadeIn(animationSpec = snap(delayMillis = 900)) togetherWith
                            scaleOut(
                                targetScale = 0.9f,
                                animationSpec = isPlayingOutSpec(),
                            ) + fadeOut(animationSpec = isPlayingOutSpec())
                    }
                            scaleOut(targetScale = 0.9f, animationSpec = isPlayingOutSpec()) +
                                fadeOut(animationSpec = isPlayingOutSpec())
                    }
                },
            ) { targetViewModel ->
                Spacer(
                    modifier =
@@ -170,11 +182,18 @@ constructor(
                            .background(
                                color = targetViewModel.backgroundColor.toColor(),
                                shape = RoundedCornerShape(12.dp),
                            ),
                            )
                            .sysuiResTag(
                                if (targetViewModel is DeviceIconViewModel.IsPlaying) {
                                    MediaOutputComponentMotionTestKeys.PLAYING_ICON_BACKGROUND_TAG
                                } else {
                                    MediaOutputComponentMotionTestKeys.IDLE_ICON_BACKGROUND_TAG
                                }
                            )
                )
            }
            transition.AnimatedContent(
                contentKey = { it.icon },
                contentKey = { Objects.hash(it.icon, it.iconColor) },
                transitionSpec = {
                    if (targetState is DeviceIconViewModel.IsPlaying) {
                        fadeIn(animationSpec = snap(delayMillis = 700)) togetherWith
@@ -189,12 +208,21 @@ constructor(
                        ) + fadeIn(animationSpec = isNotPlayingInIconSpec()) togetherWith
                            fadeOut(animationSpec = isPlayingOutSpec())
                    }
                }
            ) {
                },
            ) { targetViewModel ->
                Icon(
                    icon = it.icon,
                    tint = it.iconColor.toColor(),
                    modifier = Modifier.padding(12.dp).fillMaxSize(),
                    icon = targetViewModel.icon,
                    tint = targetViewModel.iconColor.toColor(),
                    modifier =
                        Modifier.padding(12.dp)
                            .fillMaxSize()
                            .sysuiResTag(
                                if (targetViewModel is DeviceIconViewModel.IsPlaying) {
                                    MediaOutputComponentMotionTestKeys.PLAYING_ICON_TAG
                                } else {
                                    MediaOutputComponentMotionTestKeys.IDLE_ICON_TAG
                                }
                            ),
                )
            }
        }
@@ -210,3 +238,15 @@ private fun <T> isPlayingInIconBackgroundSpec() = tween<T>(durationMillis = 400,
private fun <T> isNotPlayingOutIconSpec() = tween<T>(durationMillis = 400, delayMillis = 300)

private fun <T> isNotPlayingInIconSpec() = tween<T>(durationMillis = 400, delayMillis = 900)

@VisibleForTesting
object MediaOutputComponentMotionTestKeys {

    const val PLAYING_ICON_TAG = "PlayingIcon"
    const val PLAYING_ICON_BACKGROUND_TAG = "PlayingIconBackground"
    const val IDLE_ICON_TAG = "IdleIcon"
    const val IDLE_ICON_BACKGROUND_TAG = "IdleIconBackground"

    val isIconTransitionIdle: MotionTestValueKey<Boolean> =
        MotionTestValueKey("is_icon_transition_idle")
}
+37 −2
Original line number Diff line number Diff line
@@ -41,7 +41,9 @@ import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@@ -58,8 +60,12 @@ import com.android.compose.PlatformSliderColors
import com.android.compose.modifiers.padding
import com.android.compose.modifiers.thenIf
import com.android.systemui.Flags
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.res.R
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel
import com.google.common.annotations.VisibleForTesting
import platform.test.motion.compose.values.MotionTestValueKey
import platform.test.motion.compose.values.motionTestValues

private const val EXPAND_DURATION_MILLIS = 500
private const val COLLAPSE_EXPAND_BUTTON_DELAY_MILLIS = 350
@@ -104,6 +110,11 @@ fun ColumnVolumeSliders(
                    if (Flags.volumeRedesign()) {
                        {
                            ExpandButton(
                                modifier =
                                    Modifier.sysuiResTag(
                                        ColumnVolumeSlidersMotionTestKeys
                                            .TOGGLE_EXPANSION_BUTTON_TAG
                                    ),
                                isExpanded = isExpanded,
                                isExpandable = isExpandable,
                                onExpandedChanged = onExpandedChanged,
@@ -116,7 +127,11 @@ fun ColumnVolumeSliders(

            if (!Flags.volumeRedesign()) {
                ExpandButtonLegacy(
                    modifier = Modifier.align(Alignment.CenterEnd),
                    modifier =
                        Modifier.align(Alignment.CenterEnd)
                            .sysuiResTag(
                                ColumnVolumeSlidersMotionTestKeys.TOGGLE_EXPANSION_BUTTON_TAG
                            ),
                    isExpanded = isExpanded,
                    isExpandable = isExpandable,
                    onExpandedChanged = onExpandedChanged,
@@ -132,9 +147,22 @@ fun ColumnVolumeSliders(
            exit =
                shrinkVertically(animationSpec = tween(durationMillis = COLLAPSE_DURATION_MILLIS)),
        ) {
            val isTransitionIdle by
                remember(transition) {
                    derivedStateOf {
                        transition.currentState == transition.targetState && !transition.isRunning
                    }
                }
            // This box allows sliders to slide towards top when the container is shrinking and
            // slide from top when the container is expanding.
            Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.BottomCenter) {
            Box(
                modifier =
                    Modifier.fillMaxWidth().motionTestValues {
                        isTransitionIdle exportAs
                            ColumnVolumeSlidersMotionTestKeys.isSlidersTransitionIdle
                    },
                contentAlignment = Alignment.BottomCenter,
            ) {
                Column {
                    for (index in 1..viewModels.lastIndex) {
                        val sliderViewModel: SliderViewModel = viewModels[index]
@@ -352,3 +380,10 @@ private fun topSliderPadding(isExpandable: Boolean): State<Dp> {
        label = "TopVolumeSliderPadding",
    )
}

@VisibleForTesting
object ColumnVolumeSlidersMotionTestKeys {
    const val TOGGLE_EXPANSION_BUTTON_TAG = "sliders_toggle_button"
    val isSlidersTransitionIdle: MotionTestValueKey<Boolean> =
        MotionTestValueKey("is_sliders_transition_idle")
}
+35 −2
Original line number Diff line number Diff line
@@ -21,13 +21,31 @@ import com.android.systemui.kosmos.testScope
import org.junit.rules.RuleChain
import platform.test.motion.MotionTestRule
import platform.test.motion.compose.ComposeToolkit
import platform.test.motion.compose.FixedConfiguration
import platform.test.motion.compose.createFixedConfigurationComposeMotionTestRule
import platform.test.motion.testing.createGoldenPathManager
import platform.test.screenshot.DeviceEmulationSpec
import platform.test.screenshot.Displays
import platform.test.screenshot.PathConfig
import platform.test.screenshot.utils.compose.ComposeScreenshotTestRule

/** Create a [MotionTestRule] for motion tests of Compose-based System UI. */
/**
 * Create a [MotionTestRule] for motion tests of Compose-based System UI.
 *
 * **NOTE**: This factory uses a DeviceEmulationSpec to set a fixed density at a device level. This
 * is known to not work reliably with Robolectric. Use
 * createSysUiComposeMotionTestRuleWithFixedConfig instead, where the density is overridden in
 * compose instead.
 */
@Deprecated(
    message =
        "Use createSysUiComposeMotionTestRuleWithFixedConfig() instead. This might not work with robolectric",
    level = DeprecationLevel.WARNING,
    replaceWith =
        ReplaceWith(
            "createSysUiComposeMotionTestRuleWithFixedConfig(kosmos, deviceEmulationSpec, pathConfig,)"
        ),
)
fun createSysUiComposeMotionTestRule(
    kosmos: Kosmos,
    deviceEmulationSpec: DeviceEmulationSpec = DeviceEmulationSpec(Displays.Phone),
@@ -44,6 +62,21 @@ fun createSysUiComposeMotionTestRule(
        ComposeToolkit(composeScreenshotTestRule.composeRule, testScope),
        goldenPathManager,
        bitmapDiffer = composeScreenshotTestRule,
        extraRules = RuleChain.outerRule(composeScreenshotTestRule)
        extraRules = RuleChain.outerRule(composeScreenshotTestRule),
    )
}

/**
 * Create a [MotionTestRule] with [FixedConfiguration] for motion tests of Compose-based System UI.
 *
 * **NOTE:** [FixedConfiguration] overrides the density value in compose to provide consistent UI
 * for testing. This affects the generation of goldens.
 */
fun createSysUiComposeMotionTestRuleWithFixedConfig(
    kosmos: Kosmos,
    pathConfig: PathConfig = PathConfig(),
): MotionTestRule<ComposeToolkit> {
    val goldenPathManager =
        createGoldenPathManager("frameworks/base/packages/SystemUI/tests/goldens", pathConfig)
    return createFixedConfigurationComposeMotionTestRule(goldenPathManager, kosmos.testScope)
}
+6 −0
Original line number Diff line number Diff line
@@ -24,6 +24,8 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.android.systemui.volume.localMediaController
import com.android.systemui.volume.localPlaybackInfo
import com.android.systemui.volume.localPlaybackStateBuilder
import com.android.systemui.volume.mediaControllerRepository
import com.android.systemui.volume.mediaDeviceSessionInteractor
import com.android.systemui.volume.mediaOutputInteractor
@@ -35,6 +37,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -60,6 +63,7 @@ class MediaDeviceSessionInteractorTest : SysuiTestCase() {
    fun playbackInfo_returnsPlaybackInfo() {
        with(kosmos) {
            testScope.runTest {
                whenever(localMediaController.playbackInfo).thenReturn(localPlaybackInfo)
                val session by
                    collectLastValue(mediaOutputInteractor.defaultActiveMediaSession.filterData())
                runCurrent()
@@ -75,6 +79,8 @@ class MediaDeviceSessionInteractorTest : SysuiTestCase() {
    fun playbackState_returnsPlaybackState() {
        with(kosmos) {
            testScope.runTest {
                whenever(localMediaController.playbackState)
                    .thenReturn(localPlaybackStateBuilder.build())
                val session by
                    collectLastValue(mediaOutputInteractor.defaultActiveMediaSession.filterData())
                runCurrent()
Loading