blob: 43511e7b858bb8a7da054101ee67fd496822817a [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package ui
import (
"context"
"path/filepath"
"regexp"
"time"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/local/bundles/cros/ui/perfutil"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/chrome/ash"
"chromiumos/tast/local/chrome/display"
"chromiumos/tast/local/chrome/uiauto"
"chromiumos/tast/local/chrome/uiauto/nodewith"
"chromiumos/tast/local/input"
"chromiumos/tast/local/screenshot"
"chromiumos/tast/testing"
"chromiumos/tast/testing/hwdep"
)
func init() {
testing.AddTest(&testing.Test{
Func: TabletOperations,
Desc: "Check if the performance around user operations for tablet mode is good enough; see also go/cros-ui-perftests-cq#heading=h.fwfk0yg3teo1",
Contacts: []string{
"xdai@chromium.org",
"sammiequon@chromium.org",
"chromeos-wmp@google.com",
"mukai@chromium.org", // Tast author
},
Attr: []string{"group:mainline"},
Fixture: "chromeLoggedIn",
SoftwareDeps: []string{"chrome"},
HardwareDeps: hwdep.D(
hwdep.InternalDisplay(),
// Exclude sparky360 as its touchscreen often doesn't work well. See b/176940351.
hwdep.SkipOnModel("sparky360"),
// Scarlet devices are failing temporarily, possibly because of the display
// rotation failures. "gru" is the platform name for scarlet devices. See b/189704582.
hwdep.SkipOnPlatform("gru"),
),
Params: []testing.Param{
{
ExtraHardwareDeps: hwdep.D(hwdep.SkipOnModel(perfutil.UnstableModels...)),
},
// TODO(crbug.com/1168774): remove "unstable" once we see stability on all platforms.
{
Name: "unstable",
ExtraAttr: []string{"informational"},
ExtraHardwareDeps: hwdep.D(hwdep.Model(perfutil.UnstableModels...)),
},
},
})
}
func TabletOperations(ctx context.Context, s *testing.State) {
expects := perfutil.CreateExpectations(ctx,
"Ash.HotseatTransition.AnimationSmoothness.TransitionToHiddenHotseat",
"Ash.HotseatTransition.Drag.PresentationTime",
"Ash.HotseatWidgetAnimation.Widget.AnimationSmoothness.TransitionToHiddenHotseat",
"Ash.Overview.AnimationSmoothness.Enter.SplitView",
"Ash.Overview.AnimationSmoothness.Exit.SplitView",
"Ash.Overview.WindowDrag.PresentationTime.TabletMode",
"Ash.SplitViewResize.AnimationSmoothness.DividerAnimation",
"Ash.SplitViewResize.PresentationTime.TabletMode.WithOverview",
// Ash.TabletMode.AnimationSmoothness.{Enter,Exit} are skipped, as it is
// known to be bad. TODO(https://crbug.com/1054489): add them.
// Ash.DragWindowFromShelf.PresentationTime is skipped, it is bad
// on some devices. TODO(https://crbug.com/1168774): add this.
)
// When custom expectation value needs to be set, modify expects here.
cr := s.FixtValue().(*chrome.Chrome)
tconn, err := cr.TestAPIConn(ctx)
if err != nil {
s.Fatal("Failed to get the connection to the test API: ", err)
}
closeCtx := ctx
ctx, cancel := ctxutil.Shorten(ctx, 2*time.Second)
defer cancel()
if err := ash.CreateWindows(ctx, tconn, cr, "", 2); err != nil {
s.Fatal("Failed to create new windows: ", err)
}
r := perfutil.NewRunner(cr)
r.Runs = 5
r.RunTracing = false
s.Log("1. enter/exit tablet mode status")
// Before conducting the animation, ensure into the clamshell mode.
cleanup, err := ash.EnsureTabletModeEnabled(ctx, tconn, false)
if err != nil {
s.Fatal("Failed to ensure into clamshell mode: ", err)
}
defer cleanup(closeCtx)
// Turn windows into normal state before entering into tablet-mode.
ws, err := ash.GetAllWindows(ctx, tconn)
if err != nil {
s.Fatal("Failed to get window information: ", err)
}
for _, w := range ws {
if err := ash.SetWindowStateAndWait(ctx, tconn, w.ID, ash.WindowStateNormal); err != nil {
s.Fatalf("Failed to set window %d state to normal: %v", w.ID, err)
}
}
r.RunMultiple(ctx, s, "tablet-mode", perfutil.RunAndWaitAll(tconn, func(ctx context.Context) error {
if err := ash.SetTabletModeEnabled(ctx, tconn, true); err != nil {
return errors.Wrap(err, "failed to enable tablet mode")
}
// Wait for the top window to finish animating before changing states.
if err := ash.WaitWindowFinishAnimating(ctx, tconn, ws[0].ID); err != nil {
return errors.Wrap(err, "failed to wait for top window animation")
}
if err := ash.SetTabletModeEnabled(ctx, tconn, false); err != nil {
return errors.Wrap(err, "failed to disable tablet mode")
}
if err := ash.WaitWindowFinishAnimating(ctx, tconn, ws[0].ID); err != nil {
return errors.Wrap(err, "failed to wait for top window animation")
}
return nil
},
"Ash.TabletMode.AnimationSmoothness.Enter",
"Ash.TabletMode.AnimationSmoothness.Exit",
), perfutil.StoreAllWithHeuristics(""))
if err := ash.SetTabletModeEnabled(ctx, tconn, true); err != nil {
s.Fatal("Failed to enter into the tablet mode: ", err)
}
tsew, err := input.Touchscreen(ctx)
if err != nil {
s.Fatal("Failed to get the touch screen: ", err)
}
defer tsew.Close()
// Ensures in the landscape orientation; the following test scenario won't
// succeed when the device is in the portrait mode.
orientation, err := display.GetOrientation(ctx, tconn)
if err != nil {
s.Fatal("Failed to obtain the orientation info: ", err)
}
angle := -orientation.Angle
info, err := display.GetInternalInfo(ctx, tconn)
if err != nil {
s.Fatal("Failed to obtain the internal display info: ", err)
}
if orientation.Type == display.OrientationPortraitPrimary {
angle += 90
if err = display.SetDisplayRotationSync(ctx, tconn, info.ID, display.Rotate90); err != nil {
s.Fatal("Failed to rotate display: ", err)
}
defer display.SetDisplayRotationSync(closeCtx, tconn, info.ID, display.Rotate0)
info, err = display.GetInternalInfo(ctx, tconn)
if err != nil {
s.Fatal("Failed to obtain the internal display info: ", err)
}
}
tsew.SetRotation(angle)
tcc := tsew.NewTouchCoordConverter(info.Bounds.Size())
stw, err := tsew.NewSingleTouchWriter()
if err != nil {
s.Fatal("Failed to get the single touch event writer: ", err)
}
defer stw.Close()
// Have a browser window, and swipe from the bottom of the screen and then
// release the finger; this will switch the screen to the app-list. Then
// tap the Chrome icon in the hotseat to re-activate the window.
s.Log("2. swipe up to minimize the window")
r.RunMultiple(ctx, s, "hotseat-revealing", perfutil.RunAndWaitAll(tconn, func(ctx context.Context) (err error) {
defer func() {
if err != nil {
if captureErr := screenshot.CaptureChrome(closeCtx, cr, filepath.Join(s.OutDir(), "hotseat-failure.png")); captureErr != nil {
testing.ContextLog(ctx, "Failed to take the screenshot: ", captureErr)
}
if logErr := uiauto.LogRootDebugInfo(closeCtx, tconn, filepath.Join(s.OutDir(), "ui-tree.txt")); logErr != nil {
testing.ContextLog(ctx, "Failed to dump the UI tree: ", logErr)
}
}
}()
// Make sure the shelf bounds is stable before dragging.
if err := ash.WaitForStableShelfBounds(ctx, tconn); err != nil {
s.Fatal("Failed to wait for stable shelf bouds: ", err)
}
if err := ash.DragToShowHomescreen(ctx, tsew.Width(), tsew.Height(), stw, tconn); err != nil {
return errors.Wrap(err, "failed to show homescreen")
}
if err := ash.WaitForHotseatAnimatingToIdealState(ctx, tconn, ash.ShelfShownHomeLauncher); err != nil {
return errors.Wrap(err, "hotseat is in an unexpected state")
}
ui := uiauto.New(tconn)
// Tap the chrome icon in the app-list to re-activate the browser window.
button := nodewith.ClassName("AppListItemView").NameRegex(regexp.MustCompile("(Chrome|Chromium)"))
if err := ui.WaitUntilExists(button)(ctx); err != nil {
return errors.Wrap(err, "failed to find the Chrome icon")
}
loc, err := ui.Location(ctx, button)
if err != nil {
return errors.Wrap(err, "Chrome button is not stabilized yet")
}
if err := stw.Move(tcc.ConvertLocation(loc.CenterPoint())); err != nil {
return errors.Wrap(err, "failed to tap the leftmost icon")
}
if err := stw.End(); err != nil {
return errors.Wrap(err, "failed to release the touch")
}
var wid int
// At least one of the windows should resume, i.e. not in minimized status.
if err := ash.WaitForCondition(ctx, tconn, func(w *ash.Window) bool {
if w.State != ash.WindowStateMinimized {
wid = w.ID
return true
}
return false
}, &testing.PollOptions{Timeout: 10 * time.Second}); err != nil {
return errors.Wrap(err, "no window is in normal state")
}
// The resumed window may be still animating; wait for the animation finishes.
if err := ash.WaitWindowFinishAnimating(ctx, tconn, wid); err != nil {
return errors.Wrapf(err, "failed to wait for window (%d) animating", wid)
}
return nil
},
"Ash.DragWindowFromShelf.PresentationTime",
"Ash.HotseatTransition.AnimationSmoothness.TransitionToHiddenHotseat",
"Ash.HotseatTransition.Drag.PresentationTime",
"Ash.HotseatWidgetAnimation.Widget.AnimationSmoothness.TransitionToHiddenHotseat",
), perfutil.StoreAllWithHeuristics(""))
// This part works as:
// - enter into the overview mode
// - drag a window to the left to cause the split-view
// - move the split-view divider to the right edge of the screen
// - then wait for it to end the split-view
s.Log("3. swipe to overview, enter splitview, and resize")
r.RunMultiple(ctx, s, "overview-splitview", perfutil.RunAndWaitAll(tconn, func(ctx context.Context) (err error) {
if err := ash.SetOverviewModeAndWait(ctx, tconn, true); err != nil {
return errors.Wrap(err, "failed to enter into overview with gesture")
}
w, findErr := ash.FindWindow(ctx, tconn, func(w *ash.Window) bool { return w.OverviewInfo != nil })
if findErr != nil {
return errors.Wrap(err, "failed to find a window in overview")
}
wx, wy := tcc.ConvertLocation(w.OverviewInfo.Bounds.CenterPoint())
if err := stw.LongPressAt(ctx, wx, wy); err != nil {
return errors.Wrapf(err, "failed to activate drag of window %d", w.ID)
}
pressed := true
defer func() {
if !pressed {
return
}
if endErr := stw.End(); endErr != nil {
s.Log("Failed to release the touch: ", endErr)
if err == nil {
err = endErr
}
}
}()
leftX, _ := tcc.ConvertLocation(info.Bounds.TopLeft())
centerX, centerY := tcc.ConvertLocation(info.Bounds.CenterPoint())
right := info.Bounds.TopRight()
right.X -= info.Bounds.Width / 20
rightX, _ := tcc.ConvertLocation(right)
// Swipe to the left-center to enter into the split-view mode.
if err := stw.Swipe(ctx, wx, wy, leftX, centerY, 300*time.Millisecond); err != nil {
return errors.Wrap(err, "failed to swipe to left")
}
if err := stw.End(); err != nil {
return errors.Wrap(err, "failed to end the swipe to left")
}
pressed = false
if err := ash.WaitWindowFinishAnimating(ctx, tconn, w.ID); err != nil {
return errors.Wrap(err, "failed to wait for window animating")
}
// Exit the overview mode on the lefthand side, and then re-enter into
// overview mode.
ow, err := ash.FindFirstWindowInOverview(ctx, tconn)
if err != nil {
return errors.Wrap(err, "failed to find the window in overview mode")
}
if err := stw.Move(tcc.ConvertLocation(ow.OverviewInfo.Bounds.CenterPoint())); err != nil {
return errors.Wrapf(err, "failed to tap on window %d", ow.ID)
}
pressed = true
if err := stw.End(); err != nil {
return errors.Wrap(err, "failed to release the tap")
}
pressed = false
if err := ash.WaitForOverviewState(ctx, tconn, ash.Hidden, 5*time.Second); err != nil {
return errors.Wrap(err, "failed to wait for the overview state to be hidden")
}
if err := ash.WaitWindowFinishAnimating(ctx, tconn, ow.ID); err != nil {
return errors.Wrap(err, "failed to wait for the overview window animation")
}
if err := ash.SetOverviewModeAndWait(ctx, tconn, true); err != nil {
return errors.Wrap(err, "failed to enter into the overview mode")
}
// Swipe on the splitview divider to exit splitview.
if err := stw.Swipe(ctx, centerX, centerY, rightX, centerY, 750*time.Millisecond); err != nil {
return errors.Wrap(err, "failed to swipe to right")
}
if err := stw.End(); err != nil {
return errors.Wrap(err, "failed to end the swipe to right")
}
return ash.WaitForCondition(ctx, tconn, func(window *ash.Window) bool {
return window.ID == w.ID && !window.IsAnimating && window.State != ash.WindowStateLeftSnapped
}, &testing.PollOptions{Timeout: 2 * time.Second})
},
"Ash.Overview.AnimationSmoothness.Enter.SplitView",
"Ash.Overview.AnimationSmoothness.Exit.SplitView",
"Ash.Overview.WindowDrag.PresentationTime.TabletMode",
"Ash.SplitViewResize.AnimationSmoothness.DividerAnimation",
"Ash.SplitViewResize.PresentationTime.TabletMode.WithOverview",
), perfutil.StoreAllWithHeuristics(""))
// Check the validity of histogram data.
for _, err := range r.Values().Verify(ctx, expects) {
s.Error("Performance expectation missed: ", err)
}
// Storing the results for the future analyses.
if err := r.Values().Save(ctx, s.OutDir()); err != nil {
s.Error("Failed to save the values: ", err)
}
}