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

Commit f220fbda authored by Charles Chen's avatar Charles Chen
Browse files

Verify launch bubble from bubble menu

Test: atest
WMShellExplicitFlickerTestsBubbles:EnterBubbleViaBubbleMenuTest
Flag: TEST_ONLY
Bug: 396020056

Change-Id: I2caa4d2eb8d7af57d84700f9b55233a4b4d05be6
parent 2fad0af9
Loading
Loading
Loading
Loading
+21 −1
Original line number Diff line number Diff line
@@ -33,12 +33,32 @@ android_test {
    static_libs: [
        "FlickerTestsBase",
        "androidx.test.ext.junit",
        "flickertestapplib",
        "flag-junit",
        "flickerlib",
        "flickerlib-helpers",
        "flickerlib-trace_processor_shell",
        "flickertestapplib",
        "platform-test-annotations",
        "wm-flicker-common-app-helpers",
        "wm-shell-flicker-utils",
    ],
}

////////////////////////////////////////////////////////////////////////////////
// Begin breakdowns for WMShellExplicitFlickerTestsBubbles module

test_module_config {
    name: "WMShellExplicitFlickerTestsBubbles-CatchAll",
    base: "WMShellExplicitFlickerTestsBubbles",
    exclude_filters: [
        "com.android.wm.shell.flicker.bubbles.EnterBubbleViaBubbleMenuTest",
    ],
    test_suites: ["device-tests"],
}

test_module_config {
    name: "WMShellExplicitFlickerTestsBubbles-EnterBubbleViaBubbleMenuTest",
    base: "WMShellExplicitFlickerTestsBubbles",
    include_filters: ["com.android.wm.shell.flicker.bubbles.EnterBubbleViaBubbleMenuTest"],
    test_suites: ["device-tests"],
}
+10 −0
Original line number Diff line number Diff line
{
  "wm-devices": [
    {
      "name": "WMShellExplicitFlickerTestsBubbles-CatchAll"
    },
    {
      "name": "WMShellExplicitFlickerTestsBubbles-EnterBubbleViaBubbleMenuTest"
    }
  ]
}
 No newline at end of file
+93 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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
 *
 *      http://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.
 */

@file:JvmName("BubbleFlickerTestHelper")

package com.android.wm.shell.flicker.bubbles

import android.app.Instrumentation
import android.tools.Rotation
import android.tools.device.apphelpers.StandardAppHelper
import android.tools.flicker.rules.ChangeDisplayOrientationRule
import android.tools.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
import android.tools.io.Reader
import android.tools.traces.ConditionsFactory
import android.tools.traces.monitors.PerfettoTraceMonitor
import android.tools.traces.monitors.events.EventLogMonitor
import android.tools.traces.monitors.withTracing
import android.tools.traces.parsers.WindowManagerStateHelper
import com.android.launcher3.tapl.LauncherInstrumentation

// TODO(b/396020056): Verify bubble bar on the large screen devices.
/**
 * Called to initialize the device before the transition.
 */
fun setUpBeforeTransition(instrumentation: Instrumentation, wmHelper: WindowManagerStateHelper) {
    ChangeDisplayOrientationRule.setRotation(
        Rotation.ROTATION_0,
        instrumentation,
        clearCacheAfterParsing = false,
        wmHelper = wmHelper,
    )
    removeAllTasksButHome()
}

/**
 * A helper method to record the trace while [transition] is running.
 *
 * @sample com.android.wm.shell.flicker.bubbles.samples.runTransitionWithTraceSample
 *
 * @param transition the transition to verify.
 * @return a [Reader] that can read the trace data from.
 */
fun runTransitionWithTrace(transition: () -> Unit): Reader =
    withTracing(
        traceMonitors = listOf(
            PerfettoTraceMonitor.newBuilder()
                .enableTransitionsTrace()
                .enableLayersTrace()
                .enableWindowManagerTrace()
                .build(),
            EventLogMonitor()
        ),
        predicate = transition
    )

/**
 * Launches [testApp] into bubble via clicking bubble menu.
 *
 * @param testApp the test app to launch into bubble
 * @param tapl the [LauncherInstrumentation]
 * @param wmHelper the [WindowManagerStateHelper]
 */
fun launchBubbleViaBubbleMenu(
    testApp: StandardAppHelper,
    tapl: LauncherInstrumentation,
    wmHelper: WindowManagerStateHelper,
) {
    val allApps = tapl.goHome().switchToAllApps()
    val simpleAppIcon = allApps.getAppIcon(testApp.appName)
    // Open the bubble menu and click.
    simpleAppIcon.openMenu().bubbleMenuItem.click()

    // Wait for bubble shown.
    wmHelper
        .StateSyncBuilder()
        .add(ConditionsFactory.isWMStateComplete())
        .withAppTransitionIdle()
        .withTopVisibleApp(testApp)
        .waitForAndVerify()
}
+426 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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
 *
 *      http://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.android.wm.shell.flicker.bubbles

import android.platform.test.annotations.Presubmit
import android.platform.test.annotations.RequiresDevice
import android.platform.test.annotations.RequiresFlagsEnabled
import android.platform.test.flag.junit.CheckFlagsRule
import android.platform.test.flag.junit.DeviceFlagsValueProvider
import android.tools.Tag
import android.tools.flicker.assertions.SubjectsParser
import android.tools.flicker.subject.FlickerSubject
import android.tools.flicker.subject.events.EventLogSubject
import android.tools.flicker.subject.layers.LayerTraceEntrySubject
import android.tools.flicker.subject.layers.LayersTraceSubject
import android.tools.flicker.subject.wm.WindowManagerStateSubject
import android.tools.flicker.subject.wm.WindowManagerTraceSubject
import android.tools.io.Reader
import android.tools.traces.component.ComponentNameMatcher
import android.tools.traces.parsers.WindowManagerStateHelper
import android.tools.traces.surfaceflinger.LayerTraceEntry
import android.tools.traces.wm.WindowManagerState
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.launcher3.tapl.LauncherInstrumentation.NavigationModel
import com.android.server.wm.flicker.assertNavBarPosition
import com.android.server.wm.flicker.assertStatusBarLayerPosition
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.wm.shell.Flags
import org.junit.BeforeClass
import org.junit.FixMethodOrder
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters

/**
 * Test entering bubble via clicking bubble menu.
 *
 * To run this test: `atest WMShellExplicitFlickerTestsBubbles:EnterBubbleViaBubbleMenuTest`
 *
 * Actions:
 * ```
 *     Long press [simpleApp] icon to show [AppIconMenu].
 *     Click the bubble menu to launch [simpleApp] into bubble.
 * ```
 */
@RequiresFlagsEnabled(Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE)
@RunWith(AndroidJUnit4::class)
@RequiresDevice
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@Presubmit
class EnterBubbleViaBubbleMenuTest {

    companion object {
        private val instrumentation = InstrumentationRegistry.getInstrumentation()

        /**
         * Helper class to wait on [WindowManagerState] or [LayerTraceEntry] conditions.
         *
         * This is also used to wait for transition completes.
         */
        private val wmHelper = WindowManagerStateHelper(
            instrumentation,
            clearCacheAfterParsing = false,
        )

        /**
         * Used for building the scenario.
         */
        private val tapl: LauncherInstrumentation = LauncherInstrumentation()

        /**
         * The app used in scenario.
         */
        private val testApp = SimpleAppHelper(instrumentation)

        // TODO(b/396020056): Verify bubble scenarios in 3-button mode.
        /**
         * Indicates whether the device uses gesture navigation bar or not.
         */
        private val isGesturalNavBar = tapl.navigationModel == NavigationModel.ZERO_BUTTON

        /**
         * The reader to read trace from.
         */
        private lateinit var traceDataReader: Reader

        @BeforeClass
        @JvmStatic
        fun recordTraceWithTransition() {
            setUpBeforeTransition(instrumentation, wmHelper)

            // Run transition while recording trace.
            traceDataReader = runTransitionWithTrace {
                launchBubbleViaBubbleMenu(testApp, tapl, wmHelper)
            }

            // Finish the test app.
            testApp.exit(wmHelper)
        }
    }

    @get:Rule
    val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()

    /**
     * The WindowManager trace subject, which is equivalent to the data shown in
     * `Window Manager` tab in go/winscope.
     */
    private val wmTraceSubject: WindowManagerTraceSubject = WindowManagerTraceSubject(
        traceDataReader.readWmTrace() ?: error("Failed to read WM trace")
    )

    /**
     * The Layer trace subject, which is equivalent to the data shown in
     * `Surface Flinger` tab in go/winscope.
     */
    private val layersTraceSubject: LayersTraceSubject = LayersTraceSubject(
        traceDataReader.readLayersTrace() ?: error("Failed to read layer trace")
    )

    /**
     * The first [WindowManagerState] of the WindowManager trace.
     */
    private val wmStateSubjectAtStart: WindowManagerStateSubject

    /**
     * The last [WindowManagerState] of the WindowManager trace.
     */
    private val wmStateSubjectAtEnd: WindowManagerStateSubject

    /**
     * The first [LayerTraceEntry] of the Layers trace.
     */
    private val layerTraceEntrySubjectAtStart: LayerTraceEntrySubject

    /**
     * The last [LayerTraceEntry] of the Layers trace.
     */
    private val layerTraceEntrySubjectAtEnd: LayerTraceEntrySubject

    /**
     * Initialize subjects inherited from [FlickerSubject].
     */
    init {
        val parser = SubjectsParser(traceDataReader)
        wmStateSubjectAtStart = parser.getSubjectOfType(Tag.START)
        wmStateSubjectAtEnd = parser.getSubjectOfType(Tag.END)
        layerTraceEntrySubjectAtStart = parser.getSubjectOfType(Tag.START)
        layerTraceEntrySubjectAtEnd = parser.getSubjectOfType(Tag.END)
    }

// region Bubble related tests

    /**
     * Verifies the bubble window is visible at the end of transition.
     */
    @Test
    fun bubbleWindowIsVisibleAtEnd() {
        wmStateSubjectAtEnd.isAboveAppWindowVisible(ComponentNameMatcher.BUBBLE)
    }

    /**
     * Verifies the bubble layer is visible at the end of transition.
     */
    @Test
    fun bubbleLayerIsVisibleAtEnd() {
        layerTraceEntrySubjectAtEnd.isVisible(ComponentNameMatcher.BUBBLE)
    }

    /**
     * Verifies the bubble window becomes visible.
     */
    @Test
    fun bubbleWindowBecomesVisible() {
        wmTraceSubject
            .skipUntilFirstAssertion()
            .isAboveAppWindowInvisible(ComponentNameMatcher.BUBBLE)
            .then()
            .isAboveAppWindowVisible(ComponentNameMatcher.BUBBLE)
            .forAllEntries()
    }

    /**
     * Verifies the bubble layer becomes visible.
     */
    @Test
    fun bubbleLayerBecomesVisible() {
        layersTraceSubject
            // Bubble may not appear at the start of the transition.
            .isInvisible(ComponentNameMatcher.BUBBLE, mustExist = false)
            .then()
            .isVisible(ComponentNameMatcher.BUBBLE)
            .forAllEntries()
    }

// endregion

// region Generic tests

    /**
     * Verifies there's no flickers among all visible windows.
     *
     * In other words, all visible windows shouldn't be visible -> invisible -> visible in
     * consecutive entries
     */
    @Test
    fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
        wmTraceSubject
            .visibleWindowsShownMoreThanOneConsecutiveEntry()
            .forAllEntries()
    }

    /**
     * Verifies there's no flickers among all visible layers.
     *
     * In other words, all visible layers shouldn't be visible -> invisible -> visible in
     * consecutive entries
     */
    @Test
    fun visibleLayersShownMoreThanOneConsecutiveEntry() {
        layersTraceSubject
            .visibleLayersShownMoreThanOneConsecutiveEntry()
            .forAllEntries()
    }

// endregion

// region App Launch related tests

    /**
     * Verifies the focus changed from launcher to [testApp].
     */
    @Test
    fun focusChanges() {
        EventLogSubject(
            traceDataReader.readEventLogTrace() ?: error("Failed to read event log"),
            traceDataReader
        ).focusChanges(
            ComponentNameMatcher.LAUNCHER.toWindowName(),
            testApp.toWindowName()
        )
    }

    /**
     * Verifies the [testApp] replaces launcher to be the top window.
     */
    @Test
    fun appWindowReplacesLauncherAsTopWindow() {
        wmTraceSubject
            .isAppWindowOnTop(ComponentNameMatcher.LAUNCHER)
            .then()
            .isAppWindowOnTop(
                ComponentNameMatcher.SNAPSHOT
                    .or(ComponentNameMatcher.SPLASH_SCREEN),
                isOptional = true,
            )
            .then()
            .isAppWindowOnTop(testApp)
            .forAllEntries()
    }

    /**
     * Verifies the [testApp] is the top window at the end of transition.
     */
    @Test
    fun appWindowAsTopWindowAtEnd() {
        wmStateSubjectAtEnd.isAppWindowOnTop(testApp)
    }

    /**
     * Verifies the [testApp] becomes the top window.
     */
    @Test
    fun appWindowBecomesTopWindow() {
        wmTraceSubject
            .skipUntilFirstAssertion()
            .isAppWindowNotOnTop(testApp)
            .then()
            .isAppWindowOnTop(testApp)
            .forAllEntries()
    }

    /**
     * Verifies the [testApp] window becomes visible.
     */
    @Test
    fun appWindowBecomesVisible() {
        wmTraceSubject
            .skipUntilFirstAssertion()
            .isAppWindowInvisible(testApp)
            .then()
            .isAppWindowVisible(testApp)
            .forAllEntries()
    }

    /**
     * Verifies the [testApp] layer becomes visible.
     */
    @Test
    fun appLayerBecomesVisible() {
        layersTraceSubject
            .isInvisible(testApp)
            .then()
            .isVisible(testApp)
            .forAllEntries()
    }

    /**
     * Verifies the [testApp] window is visible at the end of transition.
     */
    @Test
    fun appWindowIsVisibleAtEnd() {
        wmStateSubjectAtEnd.isAppWindowVisible(testApp)
    }

    /**
     * Verifies the [testApp] layer is visible at the end of transition.
     */
    @Test
    fun appLayerIsVisibleAtEnd() {
        layerTraceEntrySubjectAtEnd.isVisible(testApp)
    }

    /**
     * Verifies the [testApp] layer has rounded corners
     */
    @Test
    fun appLayerHasRoundedCorner() {
        layerTraceEntrySubjectAtEnd.hasRoundedCorners(testApp)
    }

// endregion

// region System UI related tests

    /**
     * Verifies the launcher window is always visible.
     */
    @Test
    fun launcherWindowIsAlwaysVisible() {
        wmTraceSubject.isAppWindowVisible(ComponentNameMatcher.LAUNCHER).forAllEntries()
    }

    /**
     * Verifies the launcher layer is always visible.
     */
    @Test
    fun launcherLayerIsAlwaysVisible() {
        layersTraceSubject.isVisible(ComponentNameMatcher.LAUNCHER).forAllEntries()
    }

    /**
     * Verifies navigation bar layer is visible at the start and end of transition.
     */
    @Test
    fun navBarLayerIsVisibleAtStartAndEnd() {
        layerTraceEntrySubjectAtStart.isVisible(ComponentNameMatcher.NAV_BAR)
        layerTraceEntrySubjectAtEnd.isVisible(ComponentNameMatcher.NAV_BAR)
    }

    /**
     * Verifies navigation bar position at the start and end of transition.
     */
    @Test
    fun navBarLayerPositionAtStartAndEnd() {
        assertNavBarPosition(layerTraceEntrySubjectAtStart, isGesturalNavBar)
        assertNavBarPosition(layerTraceEntrySubjectAtEnd, isGesturalNavBar)
    }

    /**
     * Verifies navigation bar window is visible.
     */
    @Test
    fun navBarWindowIsAlwaysVisible() {
        wmTraceSubject
            .isAboveAppWindowVisible(ComponentNameMatcher.NAV_BAR)
            .forAllEntries()
    }

    /**
     * Verifies status bar layer is visible at the start and end of transition.
     */
    @Test
    fun statusBarLayerIsVisibleAtStartAndEnd() {
        layerTraceEntrySubjectAtStart.isVisible(ComponentNameMatcher.STATUS_BAR)
        layerTraceEntrySubjectAtEnd.isVisible(ComponentNameMatcher.STATUS_BAR)
    }

    /**
     * Verifies status bar position at the start and end of transition.
     */
    @Test
    fun statusBarLayerPositionAtStartAndEnd() {
        assertStatusBarLayerPosition(layerTraceEntrySubjectAtStart, wmStateSubjectAtStart.wmState)
        assertStatusBarLayerPosition(layerTraceEntrySubjectAtEnd, wmStateSubjectAtEnd.wmState)
    }

    /**
     * Verifies status bar window is visible.
     */
    @Test
    fun statusBarWindowIsAlwaysVisible() {
        wmTraceSubject
            .isAboveAppWindowVisible(ComponentNameMatcher.STATUS_BAR)
            .forAllEntries()
    }

// endregion
}
 No newline at end of file
+50 −0
Original line number Diff line number Diff line
@@ -14,7 +14,37 @@
 * limitations under the License.
 */

package com.android.wm.shell.flicker.bubbles
package com.android.wm.shell.flicker.bubbles.samples

// TODO(b/396020056): Add real tests in follow-up CL.
class Stub
 No newline at end of file
import android.tools.flicker.subject.events.EventLogSubject
import android.tools.flicker.subject.layers.LayersTraceSubject
import android.tools.flicker.subject.wm.WindowManagerTraceSubject
import com.android.wm.shell.flicker.bubbles.runTransitionWithTrace

/**
 * Sample to illustrate how to use [runTransitionWithTrace].
 */
fun runTransitionWithTraceSample() {
    val reader = runTransitionWithTrace {
        // Add transition here
    }

    // Extract [WindowManagerTraceSubject]
    val wmTraceSubject = WindowManagerTraceSubject(
        reader.readWmTrace() ?: error("Failed to read WM trace")
    )

    // Extract [LayerTraceSubject]
    val layersTraceSubject = LayersTraceSubject(
        reader.readLayersTrace() ?: error("Failed to read Layers trace")
    )

    // Extract [EventLogSubject]
    val eventLogSubject = EventLogSubject(
        reader.readEventLogTrace() ?: error("Failed to read event log trace"),
        reader,
    )

    // Read CUJ Trace
    val cujTrace = reader.readCujTrace()
}
 No newline at end of file
Loading